/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2001 - 2009 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.modules.output.table.html;
import java.io.IOException;
import java.text.NumberFormat;
import org.pentaho.reporting.engine.classic.core.AttributeNames;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
import org.pentaho.reporting.engine.classic.core.ImageContainer;
import org.pentaho.reporting.engine.classic.core.InvalidReportStateException;
import org.pentaho.reporting.engine.classic.core.ReportAttributeMap;
import org.pentaho.reporting.engine.classic.core.URLImageContainer;
import org.pentaho.reporting.engine.classic.core.imagemap.ImageMap;
import org.pentaho.reporting.engine.classic.core.imagemap.parser.ImageMapWriter;
import org.pentaho.reporting.engine.classic.core.layout.model.BlockRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.CanvasRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.InlineRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.LayoutNodeTypes;
import org.pentaho.reporting.engine.classic.core.layout.model.ParagraphRenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderNode;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableReplacedContent;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableReplacedContentBox;
import org.pentaho.reporting.engine.classic.core.layout.model.RenderableText;
import org.pentaho.reporting.engine.classic.core.layout.model.SpacerRenderNode;
import org.pentaho.reporting.engine.classic.core.layout.output.OutputProcessorMetaData;
import org.pentaho.reporting.engine.classic.core.layout.output.RenderUtility;
import org.pentaho.reporting.engine.classic.core.layout.text.GlyphList;
import org.pentaho.reporting.engine.classic.core.modules.output.table.base.DefaultTextExtractor;
import org.pentaho.reporting.engine.classic.core.modules.output.table.html.helper.HtmlOutputProcessingException;
import org.pentaho.reporting.engine.classic.core.modules.output.table.html.helper.StyleBuilder;
import org.pentaho.reporting.engine.classic.core.modules.output.table.html.helper.StyleManager;
import org.pentaho.reporting.engine.classic.core.style.ElementStyleKeys;
import org.pentaho.reporting.engine.classic.core.style.StyleSheet;
import org.pentaho.reporting.engine.classic.core.util.geom.StrictBounds;
import org.pentaho.reporting.engine.classic.core.util.geom.StrictGeomUtility;
import org.pentaho.reporting.libraries.base.util.FastStack;
import org.pentaho.reporting.libraries.base.util.StringUtils;
import org.pentaho.reporting.libraries.repository.ContentIOException;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.factory.drawable.DrawableWrapper;
import org.pentaho.reporting.libraries.xmlns.common.AttributeList;
import org.pentaho.reporting.libraries.xmlns.writer.CharacterEntityParser;
import org.pentaho.reporting.libraries.xmlns.writer.HtmlCharacterEntities;
import org.pentaho.reporting.libraries.xmlns.writer.XmlWriter;
import org.pentaho.reporting.libraries.xmlns.writer.XmlWriterSupport;
/**
* Creation-Date: 02.11.2007, 15:58:29
*
* @author Thomas Morgner
*/
public class HtmlTextExtractor extends DefaultTextExtractor
{
private static class ElementProcessInfo
{
private boolean link;
private boolean extraAttributes;
private ElementProcessInfo(final boolean link, final boolean extraAttributes)
{
this.link = link;
this.extraAttributes = extraAttributes;
}
public boolean isLink()
{
return link;
}
public boolean isExtraAttributes()
{
return extraAttributes;
}
}
private static final String DIV_TAG = "div";
private static final String HREF_ATTR = "href";
private static final String TARGET_ATTR = "target";
private static final String TITLE_ATTR = "title";
private static final String A_TAG = "a";
private static final String BR_TAG = "br";
private static final String SPAN_TAG = "span";
private static final String IMG_TAG = "img";
private static final String SRC_ATTR = "src";
private static final String USEMAP_ATTR = "usemap";
private static final String PT_UNIT = "pt";
private static final String WIDTH_STYLE = "width";
private static final String HEIGHT_STYLE = "height";
private static final String ALT_ATTR = "alt";
private FastStack processStack;
private OutputProcessorMetaData metaData;
private XmlWriter xmlWriter;
private StyleManager styleManager;
private StyleBuilder styleBuilder;
private HtmlContentGenerator contentGenerator;
private CharacterEntityParser characterEntityParser;
private boolean result;
private boolean safariLengthFix;
private RenderBox firstElement;
private boolean useWhitespacePreWrap;
public HtmlTextExtractor(final OutputProcessorMetaData metaData,
final XmlWriter xmlWriter,
final StyleManager styleManager,
final HtmlContentGenerator contentGenerator)
{
super(metaData);
if (xmlWriter == null)
{
throw new NullPointerException();
}
if (styleManager == null)
{
throw new NullPointerException();
}
if (contentGenerator == null)
{
throw new NullPointerException();
}
this.contentGenerator = contentGenerator;
this.processStack = new FastStack();
this.metaData = metaData;
this.xmlWriter = xmlWriter;
this.styleManager = styleManager;
this.styleBuilder = new StyleBuilder();
this.characterEntityParser = HtmlCharacterEntities.getEntityParser();
this.safariLengthFix = ("true".equals(ClassicEngineBoot.getInstance().getGlobalConfig().getConfigProperty
("org.pentaho.reporting.engine.classic.core.modules.output.table.html.SafariLengthHack")));
this.useWhitespacePreWrap = ("true".equals(ClassicEngineBoot.getInstance().getGlobalConfig().getConfigProperty
("org.pentaho.reporting.engine.classic.core.modules.output.table.html.UseWhitespacePreWrap")));
}
public boolean performOutput(final RenderBox content, final boolean ignoreFirstElement) throws IOException
{
styleBuilder.clear();
clearText();
setRawResult(null);
result = false;
processStack.clear();
if (ignoreFirstElement)
{
firstElement = content;
}
try
{
if (content.getNodeType() == LayoutNodeTypes.TYPE_BOX_PARAGRAPH)
{
processParagraphCell((ParagraphRenderBox) content);
}
else if (content.getNodeType() == LayoutNodeTypes.TYPE_BOX_CONTENT)
{
processRenderableContent((RenderableReplacedContentBox) content);
}
else
{
processBoxChilds(content);
}
}
finally
{
processStack.clear();
}
return result;
}
/**
* Prints the contents of a canvas box. This can happen only once per cell, as every canvas box creates its
* own cell at some point. If for some strange reason a canvas box appears in the middle of a box-structure,
* your layouter is probably a mess and this method will treat the box as a generic content container.
*
* @param box the canvas box
* @return true, if the child content will be processed, false otherwise.
*/
public boolean startCanvasBox(final CanvasRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return false;
}
try
{
final ReportAttributeMap attrs = box.getAttributes();
final boolean extraAttributes;
if (firstElement != box)
{
final AttributeList attrList = new AttributeList();
HtmlPrinter.applyHtmlAttributes(attrs, attrList);
if (attrList.isEmpty() == false)
{
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, DIV_TAG, attrList, XmlWriterSupport.OPEN);
}
extraAttributes = false;
}
else
{
extraAttributes = true;
}
final Object rawContent = attrs.getAttribute(AttributeNames.Html.NAMESPACE,
AttributeNames.Html.EXTRA_RAW_CONTENT);
if (rawContent != null)
{
xmlWriter.writeText(String.valueOf(rawContent));
}
final StyleSheet styleSheet = box.getStyleSheet();
final String target = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TARGET);
if (target != null)
{
final String window = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_WINDOW);
final AttributeList linkAttr = new AttributeList();
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, HREF_ATTR, target);
if (window != null && StringUtils.startsWithIgnoreCase(target, "javascript:") == false)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TARGET_ATTR, normalizeWindow(window));
}
final String title = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TITLE);
if (title != null)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TITLE_ATTR, title);
}
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, linkAttr, XmlWriterSupport.OPEN);
processStack.push(new ElementProcessInfo(true, extraAttributes));
}
else
{
processStack.push(new ElementProcessInfo(false, extraAttributes));
}
if (Boolean.TRUE.equals(attrs.getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.SURPRESS_CONTENT)))
{
return false;
}
return true;
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
private String normalizeWindow(final String window)
{
if ("_top".equalsIgnoreCase(window))
{
return "_top";
}
if ("_self".equalsIgnoreCase(window))
{
return "_self";
}
if ("_parent".equalsIgnoreCase(window))
{
return "_parent";
}
if ("_blank".equalsIgnoreCase(window))
{
return "_blank";
}
return window;
}
public void finishCanvasBox(final CanvasRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return;
}
try
{
final ElementProcessInfo info = (ElementProcessInfo) processStack.pop();
if (info.isLink())
{
xmlWriter.writeCloseTag();
}
final Object rawFooterContent = box.getAttributes().getAttribute(AttributeNames.Html.NAMESPACE,
AttributeNames.Html.EXTRA_RAW_FOOTER_CONTENT);
if (rawFooterContent != null)
{
xmlWriter.writeText(String.valueOf(rawFooterContent));
}
if (info.isExtraAttributes())
{
xmlWriter.writeCloseTag();
}
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
/**
* Prints a paragraph cell. This is a special entry point used by the processContent method and is never
* called from elsewhere. This method assumes that the attributes of the paragraph have been processed as
* part of the table-cell processing.
*
* @param box the paragraph box
* @throws IOException if an IO error occured.
*/
protected void processParagraphCell(final ParagraphRenderBox box) throws IOException
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return;
}
final StyleSheet styleSheet = box.getStyleSheet();
final String target = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TARGET);
if (target != null)
{
final String window = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_WINDOW);
final AttributeList linkAttr = new AttributeList();
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, HREF_ATTR, target);
if (window != null && StringUtils.startsWithIgnoreCase(target, "javascript:") == false)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TARGET_ATTR, normalizeWindow(window));
}
final String title = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TITLE);
if (title != null)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TITLE_ATTR, title);
}
styleManager.updateStyle(HtmlPrinter.produceTextStyle
(styleBuilder, box, false, false, safariLengthFix, useWhitespacePreWrap),
linkAttr);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, linkAttr, XmlWriterSupport.OPEN);
}
if (Boolean.TRUE.equals
(box.getAttributes().getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.SURPRESS_CONTENT)) == false)
{
processParagraphChilds(box);
}
if (target != null)
{
xmlWriter.writeCloseTag(); // closing the span ..
}
}
protected void addEmptyBreak()
{
try
{
xmlWriter.writeText(" ");
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
protected void addSoftBreak()
{
try
{
xmlWriter.writeText(" ");
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
protected void addLinebreak()
{
try
{
result = true;
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, BR_TAG, XmlWriterSupport.CLOSE);
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
protected boolean startBlockBox(final BlockRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return false;
}
try
{
final AttributeList attrList = new AttributeList();
final ReportAttributeMap attrs = box.getAttributes();
if (firstElement != box)
{
HtmlPrinter.applyHtmlAttributes(attrs, attrList);
styleManager.updateStyle(HtmlPrinter.produceTextStyle
(styleBuilder, box, true, false, safariLengthFix, useWhitespacePreWrap), attrList);
}
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, DIV_TAG, attrList, XmlWriterSupport.OPEN);
final Object rawContent = attrs.getAttribute(AttributeNames.Html.NAMESPACE,
AttributeNames.Html.EXTRA_RAW_CONTENT);
if (rawContent != null)
{
xmlWriter.writeText(String.valueOf(rawContent));
}
final StyleSheet styleSheet = box.getStyleSheet();
if (firstElement != box)
{
final String anchor = (String) styleSheet.getStyleProperty(ElementStyleKeys.ANCHOR_NAME);
if (anchor != null)
{
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, "name", anchor, XmlWriterSupport.CLOSE);
}
}
final String target = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TARGET);
if (target != null)
{
final String window = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_WINDOW);
final AttributeList linkAttr = new AttributeList();
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, HREF_ATTR, target);
if (window != null && StringUtils.startsWithIgnoreCase(target, "javascript:") == false)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TARGET_ATTR, normalizeWindow(window));
}
final String title = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TITLE);
if (title != null)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TITLE_ATTR, title);
}
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, linkAttr, XmlWriterSupport.OPEN);
processStack.push(new ElementProcessInfo(true, true));
}
else
{
processStack.push(new ElementProcessInfo(false, true));
}
if (Boolean.TRUE.equals(attrs.getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.SURPRESS_CONTENT)))
{
return false;
}
return true;
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
protected void finishBlockBox(final BlockRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return;
}
try
{
final ElementProcessInfo info = (ElementProcessInfo) processStack.pop();
if (info.isLink())
{
// close anchor tag
xmlWriter.writeCloseTag();
}
final Object rawFooterContent = box.getAttributes().getAttribute(AttributeNames.Html.NAMESPACE,
AttributeNames.Html.EXTRA_RAW_FOOTER_CONTENT);
if (rawFooterContent != null)
{
xmlWriter.writeText(String.valueOf(rawFooterContent));
}
// close mandatory DIV
xmlWriter.writeCloseTag();
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
/**
* Like a canvas box, a row-box should be split into several cells already. Therefore we treat it as a generic
* content container instead.
*
* @param box
* @return
*/
protected boolean startRowBox(final RenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return false;
}
try
{
final AttributeList attrList = new AttributeList();
final ReportAttributeMap attrs = box.getAttributes();
if (firstElement != box)
{
HtmlPrinter.applyHtmlAttributes(attrs, attrList);
styleManager.updateStyle(HtmlPrinter.produceTextStyle
(styleBuilder, box, true, false, safariLengthFix, useWhitespacePreWrap), attrList);
}
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, DIV_TAG, attrList, XmlWriterSupport.OPEN);
final Object rawContent = attrs.getAttribute(AttributeNames.Html.NAMESPACE,
AttributeNames.Html.EXTRA_RAW_CONTENT);
if (rawContent != null)
{
xmlWriter.writeText(String.valueOf(rawContent));
}
final StyleSheet styleSheet = box.getStyleSheet();
if (firstElement != box)
{
final String anchor = (String) styleSheet.getStyleProperty(ElementStyleKeys.ANCHOR_NAME);
if (anchor != null)
{
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, "name", anchor, XmlWriterSupport.CLOSE);
}
}
final String target = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TARGET);
if (target != null)
{
final String window = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_WINDOW);
final AttributeList linkAttr = new AttributeList();
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, HREF_ATTR, target);
if (window != null && StringUtils.startsWithIgnoreCase(target, "javascript:") == false)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TARGET_ATTR, normalizeWindow(window));
}
final String title = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TITLE);
if (title != null)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TITLE_ATTR, title);
}
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, linkAttr, XmlWriterSupport.OPEN);
processStack.push(new ElementProcessInfo(true, true));
}
else
{
processStack.push(new ElementProcessInfo(false, true));
}
if (Boolean.TRUE.equals(attrs.getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.SURPRESS_CONTENT)))
{
return false;
}
return true;
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
protected void finishRowBox(final RenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return;
}
try
{
final ElementProcessInfo info = (ElementProcessInfo) processStack.pop();
if (info.isLink())
{
// close anchor tag
xmlWriter.writeCloseTag();
}
final Object rawFooterContent = box.getAttributes().getAttribute(AttributeNames.Html.NAMESPACE,
AttributeNames.Html.EXTRA_RAW_FOOTER_CONTENT);
if (rawFooterContent != null)
{
xmlWriter.writeText(String.valueOf(rawFooterContent));
}
xmlWriter.writeCloseTag();
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
protected boolean startInlineBox(final InlineRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return false;
}
try
{
final boolean extraAttributes;
final ReportAttributeMap attrs = box.getAttributes();
if (firstElement != box)
{
final AttributeList attrList = new AttributeList();
HtmlPrinter.applyHtmlAttributes(attrs, attrList);
styleManager.updateStyle(HtmlPrinter.produceTextStyle
(styleBuilder, box, true, true, safariLengthFix, useWhitespacePreWrap), attrList);
if (attrList.isEmpty() == false)
{
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, SPAN_TAG, attrList, XmlWriterSupport.OPEN);
extraAttributes = true;
}
else
{
extraAttributes = false;
}
}
else
{
extraAttributes = false;
}
final Object rawContent = attrs.getAttribute(AttributeNames.Html.NAMESPACE,
AttributeNames.Html.EXTRA_RAW_CONTENT);
if (rawContent != null)
{
xmlWriter.writeText(String.valueOf(rawContent));
}
final StyleSheet styleSheet = box.getStyleSheet();
if (firstElement != box)
{
final String anchor = (String) styleSheet.getStyleProperty(ElementStyleKeys.ANCHOR_NAME);
if (anchor != null)
{
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, "name", anchor, XmlWriterSupport.CLOSE);
}
}
final String target = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TARGET);
if (target != null)
{
final String window = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_WINDOW);
final AttributeList linkAttr = new AttributeList();
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, HREF_ATTR, target);
if (window != null && StringUtils.startsWithIgnoreCase(target, "javascript:") == false)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TARGET_ATTR, normalizeWindow(window));
}
final String title = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TITLE);
if (title != null)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TITLE_ATTR, title);
}
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, linkAttr, XmlWriterSupport.OPEN);
processStack.push(new ElementProcessInfo(true, extraAttributes));
}
else
{
processStack.push(new ElementProcessInfo(false, extraAttributes));
}
if (Boolean.TRUE.equals(attrs.getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.SURPRESS_CONTENT)))
{
return false;
}
return true;
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
protected void finishInlineBox(final InlineRenderBox box)
{
if (box.getStaticBoxLayoutProperties().isVisible() == false)
{
return;
}
try
{
final ElementProcessInfo info = (ElementProcessInfo) processStack.pop();
if (info.isLink())
{
// close anchor tag
xmlWriter.writeCloseTag();
}
final Object rawFooterContent = box.getAttributes().getAttribute(AttributeNames.Html.NAMESPACE,
AttributeNames.Html.EXTRA_RAW_FOOTER_CONTENT);
if (rawFooterContent != null)
{
xmlWriter.writeText(String.valueOf(rawFooterContent));
}
if (info.isExtraAttributes())
{
xmlWriter.writeCloseTag();
}
}
catch (IOException e)
{
throw new HtmlOutputProcessingException("Failed to perform IO", e);
}
}
protected void processOtherNode(final RenderNode node)
{
try
{
if (node.getNodeType() == LayoutNodeTypes.TYPE_NODE_TEXT)
{
super.processOtherNode(node);
return;
}
if (node.isVirtualNode())
{
return;
}
if (node.getNodeType() == LayoutNodeTypes.TYPE_NODE_SPACER)
{
final SpacerRenderNode spacer = (SpacerRenderNode) node;
final int count = Math.max(1, spacer.getSpaceCount());
for (int i = 0; i < count; i++)
{
xmlWriter.writeText(" ");
}
}
}
catch (IOException e)
{
throw new RuntimeException("Failed", e);
}
}
protected void processRenderableContent(final RenderableReplacedContentBox node)
{
try
{
final ReportAttributeMap map = node.getAttributes();
final AttributeList attrs = new AttributeList();
HtmlPrinter.applyHtmlAttributes(map, attrs);
if (attrs.isEmpty() == false)
{
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, DIV_TAG, attrs, XmlWriterSupport.OPEN);
}
final StyleSheet styleSheet = node.getStyleSheet();
final String anchor = (String) styleSheet.getStyleProperty(ElementStyleKeys.ANCHOR_NAME);
if (anchor != null)
{
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, "name", anchor, XmlWriterSupport.CLOSE);
}
final String target = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TARGET);
if (target != null)
{
final String window = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_WINDOW);
final AttributeList linkAttr = new AttributeList();
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, HREF_ATTR, target);
if (window != null && StringUtils.startsWithIgnoreCase(target, "javascript:") == false)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TARGET_ATTR, normalizeWindow(window));
}
final String title = (String) styleSheet.getStyleProperty(ElementStyleKeys.HREF_TITLE);
if (title != null)
{
linkAttr.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TITLE_ATTR, title);
}
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, A_TAG, linkAttr, XmlWriterSupport.OPEN);
}
processReplacedContent(node);
if (target != null)
{
xmlWriter.writeCloseTag();
}
if (attrs.isEmpty() == false)
{
xmlWriter.writeCloseTag();
}
}
catch (IOException e)
{
throw new RuntimeException("Failed", e);
}
catch (ContentIOException e)
{
throw new RuntimeException("Failed", e);
}
}
/**
* @noinspection StringConcatenation
*/
private void processReplacedContent(final RenderableReplacedContentBox node) throws IOException, ContentIOException
{
final RenderableReplacedContent rc = node.getContent();
final Object rawObject = rc.getRawObject();
// We have to do three things here. First, w have to check what kind
// of content we deal with.
// todo: Check whether the raw-object would be understood by the browser
if (rawObject instanceof URLImageContainer)
{
final URLImageContainer urlImageContainer = (URLImageContainer) rawObject;
final ResourceKey source = urlImageContainer.getResourceKey();
if (source != null)
{
// Cool, we have access to the raw-data. Thats always nice as we
// dont have to recode the whole thing. We can only recode images, not drawables.
if (contentGenerator.isRegistered(source) == false)
{
// Write image reference; return the name of the reference. This method will
// return null, if the image is not recognized (it is no JPG, PNG or GIF image)
final String name = contentGenerator.writeRaw(source);
if (name != null)
{
// Write image reference ..
final AttributeList attrList = new AttributeList();
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, SRC_ATTR, name);
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, "border", "0");
// width and height and scaling and so on ..
final StyleBuilder imgStyle = produceImageStyle(node);
if (imgStyle == null)
{
final AttributeList clipAttrList = new AttributeList();
final StyleBuilder divStyle = produceClipStyle(node);
styleManager.updateStyle(divStyle, clipAttrList);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, DIV_TAG, clipAttrList, XmlWriter.OPEN);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, IMG_TAG, attrList, XmlWriter.CLOSE);
xmlWriter.writeCloseTag();
}
else
{
styleManager.updateStyle(imgStyle, attrList);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, IMG_TAG, attrList, XmlWriter.CLOSE);
}
contentGenerator.registerContent(source, name);
result = true;
return;
}
else
{
// Mark this object as non-readable. This way we dont retry the failed operation
// over and over again.
contentGenerator.registerFailure(source);
}
}
else
{
final String cachedName = contentGenerator.getRegisteredName(source);
if (cachedName != null)
{
final AttributeList attrList = new AttributeList();
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, SRC_ATTR, cachedName);
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, "border", "0");
// width and height and scaling and so on ..
final StyleBuilder imgStyle = produceImageStyle(node);
if (imgStyle == null)
{
final AttributeList clipAttrList = new AttributeList();
final StyleBuilder divStyle = produceClipStyle(node);
styleManager.updateStyle(divStyle, clipAttrList);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, DIV_TAG, clipAttrList, XmlWriter.OPEN);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, IMG_TAG, attrList, XmlWriter.CLOSE);
xmlWriter.writeCloseTag();
}
else
{
styleManager.updateStyle(imgStyle, attrList);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, IMG_TAG, attrList, XmlWriter.CLOSE);
}
result = true;
return;
}
}
}
}
// Fallback: (At the moment, we only support drawables and images.)
final ReportAttributeMap attributes = node.getAttributes();
if (rawObject instanceof ImageContainer)
{
final String type = RenderUtility.getEncoderType(attributes);
final float quality = RenderUtility.getEncoderQuality(attributes);
// Make it a PNG file ..
//xmlWriter.writeComment("Image content source:" + source);
final String name = contentGenerator.writeImage((ImageContainer) rawObject, type, quality, true);
if (name != null)
{
// Write image reference ..
final AttributeList attrList = new AttributeList();
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, SRC_ATTR, name);
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, "border", "0");
final Object titleText = attributes.getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.TITLE);
if (titleText != null)
{
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TITLE_ATTR, String.valueOf(titleText));
}
final Object altText = attributes.getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.ALT);
if (altText != null)
{
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, ALT_ATTR, String.valueOf(altText));
}
// width and height and scaling and so on ..
final StyleBuilder imgStyle = produceImageStyle(node);
if (imgStyle == null)
{
final AttributeList clipAttrList = new AttributeList();
final StyleBuilder divStyle = produceClipStyle(node);
styleManager.updateStyle(divStyle, clipAttrList);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, DIV_TAG, clipAttrList, XmlWriterSupport.OPEN);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, IMG_TAG, attrList, XmlWriterSupport.CLOSE);
xmlWriter.writeCloseTag();
}
else
{
styleManager.updateStyle(imgStyle, attrList);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, IMG_TAG, attrList, XmlWriterSupport.CLOSE);
}
result = true;
}
return;
}
if (rawObject instanceof DrawableWrapper)
{
// render it into an Buffered image and make it a PNG file.
final DrawableWrapper drawable = (DrawableWrapper) rawObject;
final StrictBounds cb = new StrictBounds(node.getX(), node.getY(), node.getWidth(), node.getHeight());
final ImageContainer image = RenderUtility.createImageFromDrawable(drawable, cb, node,
metaData);
if (image == null)
{
//xmlWriter.writeComment("Drawable content [No image generated]:" + source);
return;
}
final String type = RenderUtility.getEncoderType(attributes);
final float quality = RenderUtility.getEncoderQuality(attributes);
final String name = contentGenerator.writeImage(image, type, quality, true);
if (name == null)
{
//xmlWriter.writeComment("Drawable content [No image written]:" + source);
return;
}
//xmlWriter.writeComment("Drawable content:" + source);
// Write image reference ..
final ImageMap imageMap;
final AttributeList attrList = new AttributeList();
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, SRC_ATTR, name);
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, "border", "0");
final Object imageMapNameOverride = attributes.getAttribute
(AttributeNames.Html.NAMESPACE, AttributeNames.Html.IMAGE_MAP_OVERRIDE);
if (imageMapNameOverride != null)
{
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, USEMAP_ATTR, String.valueOf(imageMapNameOverride));
imageMap = null;
}
else
{
// only generate a image map, if the user does not specify their own onw via the override.
// Of course, they would have to provide the map by other means as well.
imageMap = RenderUtility.extractImageMap(node);
if (imageMap != null)
{
final String mapName = imageMap.getAttribute(HtmlPrinter.XHTML_NAMESPACE, "name");
if (mapName != null)
{
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, USEMAP_ATTR, "#" + mapName);
}
else
{
final String generatedName = "generated_" + name + "_map"; //NON-NLS
imageMap.setAttribute(HtmlPrinter.XHTML_NAMESPACE, "name", generatedName);
//noinspection MagicCharacter
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, USEMAP_ATTR, '#' + generatedName);//NON-NLS
}
}
}
final Object titleText = attributes.getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.TITLE);
if (titleText != null)
{
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, TITLE_ATTR, String.valueOf(titleText));
}
final Object altText = attributes.getAttribute(AttributeNames.Html.NAMESPACE, AttributeNames.Html.ALT);
if (altText != null)
{
attrList.setAttribute(HtmlPrinter.XHTML_NAMESPACE, ALT_ATTR, String.valueOf(altText));
}
// width and height and scaling and so on ..
final StyleBuilder imgStyle = produceImageStyle(node);
if (imgStyle == null)
{
final AttributeList clipAttrList = new AttributeList();
final StyleBuilder divStyle = produceClipStyle(node);
styleManager.updateStyle(divStyle, clipAttrList);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, DIV_TAG, clipAttrList, XmlWriterSupport.OPEN);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, IMG_TAG, attrList, XmlWriterSupport.CLOSE);
xmlWriter.writeCloseTag();
}
else
{
styleManager.updateStyle(imgStyle, attrList);
xmlWriter.writeTag(HtmlPrinter.XHTML_NAMESPACE, IMG_TAG, attrList, XmlWriterSupport.CLOSE);
}
if (imageMap != null)
{
ImageMapWriter.writeImageMap(xmlWriter, imageMap, RenderUtility.getNormalizationScale(metaData));
}
result = true;
}
}
private StyleBuilder produceClipStyle(final RenderableReplacedContentBox rc)
{
styleBuilder.clear(); // cuts down on object creation
final long nodeWidth = rc.getWidth();
final long nodeHeight = rc.getHeight();
final NumberFormat pointConverter = styleBuilder.getPointConverter();
styleBuilder.append("overflow", "hidden"); //NON-NLS
styleBuilder.append(WIDTH_STYLE, pointConverter.format//NON-NLS
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue(nodeWidth), safariLengthFix)), PT_UNIT);
styleBuilder.append(HEIGHT_STYLE, pointConverter.format//NON-NLS
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue(nodeHeight), safariLengthFix)), PT_UNIT);
return styleBuilder;
}
/**
* Populates the style builder with the style information for the image based on the RenderableReplacedContent
*
* @param rc th renderable content node.
* @return the style-builder with the image style or null, if the image must be clipped.
*/
private StyleBuilder produceImageStyle(final RenderableReplacedContentBox rc)
{
styleBuilder.clear(); // cuts down on object creation
final NumberFormat pointConverter = styleBuilder.getPointConverter();
final RenderableReplacedContent content = rc.getContent();
final long contentWidth = content.getContentWidth();
final long nodeWidth = rc.getWidth();
final long contentHeight = content.getContentHeight();
final long nodeHeight = rc.getHeight();
final StyleSheet styleSheet = rc.getStyleSheet();
if (styleSheet.getBooleanStyleProperty(ElementStyleKeys.SCALE))
{
if (styleSheet.getBooleanStyleProperty(ElementStyleKeys.KEEP_ASPECT_RATIO) &&
(contentWidth > 0 && contentHeight > 0))
{
final double scaleFactor = Math.min(nodeWidth / (double) contentWidth, nodeHeight / (double) contentHeight);
styleBuilder.append(WIDTH_STYLE, pointConverter.format
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue((long) (contentWidth * scaleFactor)),
safariLengthFix)), PT_UNIT);
styleBuilder.append(HEIGHT_STYLE, pointConverter.format
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue((long) (contentHeight * scaleFactor)),
safariLengthFix)), PT_UNIT);
}
else
{
styleBuilder.append(WIDTH_STYLE, pointConverter.format
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue(nodeWidth), safariLengthFix)), PT_UNIT);
styleBuilder.append(HEIGHT_STYLE, pointConverter.format
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue(nodeHeight), safariLengthFix)), PT_UNIT);
}
}
else
{
// for plain drawable content, there is no intrinsic-width or height, so we have to use the computed
// width and height instead.
if (contentWidth > nodeWidth || contentHeight > nodeHeight)
{
// There is clipping involved. The img-element does *not* receive a width or height property.
// the width and height is applied to an external DIV element instead.
return null;
}
if (contentWidth == 0 && contentHeight == 0)
{
// Drawable content has no intrinsic height or width, therefore we must not use the content size at all.
styleBuilder.append(WIDTH_STYLE, pointConverter.format
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue(nodeWidth), safariLengthFix)), PT_UNIT);
styleBuilder.append(HEIGHT_STYLE, pointConverter.format
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue(nodeHeight), safariLengthFix)), PT_UNIT);
}
else
{
final long width = Math.min(nodeWidth, contentWidth);
final long height = Math.min(nodeHeight, contentHeight);
styleBuilder.append(WIDTH_STYLE, pointConverter.format
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue(width), safariLengthFix)), PT_UNIT);
styleBuilder.append(HEIGHT_STYLE, pointConverter.format
(HtmlPrinter.fixLengthForSafari(StrictGeomUtility.toExternalValue(height), safariLengthFix)), PT_UNIT);
}
}
return styleBuilder;
}
protected void drawText(final RenderableText renderableText, final long contentX2)
{
try
{
if (renderableText.getLength() == 0)
{
// This text is empty.
return;
}
if (renderableText.isNodeVisible(getParagraphBounds(), isOverflowX(), isOverflowY()) == false)
{
return;
}
final String text;
if (contentX2 >= (renderableText.getX() + renderableText.getWidth()))
{
final GlyphList gs = renderableText.getGlyphs();
text = gs.getText(renderableText.getOffset(), renderableText.getLength(), getCodePointBuffer());
}
else
{
final GlyphList gs = renderableText.getGlyphs();
final int maxLength = computeMaximumTextSize(renderableText, contentX2);
text = gs.getText(renderableText.getOffset(), maxLength, getCodePointBuffer());
}
if (text.length() > 0)
{
xmlWriter.writeText(characterEntityParser.encodeEntities(text));
if (text.trim().length() > 0)
{
result = true;
}
clearText();
}
}
catch (IOException ioe)
{
throw new InvalidReportStateException("Failed to write text", ioe);
}
}
}