/** * Copyright (C) 2011-2015 The XDocReport Team <xdocreport@googlegroups.com> * * All rights reserved. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package fr.opensagres.poi.xwpf.converter.xhtml.internal; import static fr.opensagres.poi.xwpf.converter.core.utils.DxaUtil.emu2points; import static fr.opensagres.poi.xwpf.converter.xhtml.internal.XHTMLConstants.*; import static fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStylePropertyConstants.HEIGHT; import static fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStylePropertyConstants.MARGIN_BOTTOM; import static fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStylePropertyConstants.MARGIN_LEFT; import static fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStylePropertyConstants.MARGIN_RIGHT; import static fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStylePropertyConstants.MARGIN_TOP; import static fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStylePropertyConstants.WIDTH; import java.io.IOException; import java.math.BigInteger; import java.util.List; import org.apache.poi.xwpf.usermodel.*; import org.apache.xmlbeans.XmlException; import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D; import org.openxmlformats.schemas.drawingml.x2006.picture.CTPicture; import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFromH.Enum; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBookmark; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHdrFtrRef; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPTab; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTRPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTabs; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTText; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalAlignRun; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import fr.opensagres.poi.xwpf.converter.core.BorderSide; import fr.opensagres.poi.xwpf.converter.core.Color; import fr.opensagres.poi.xwpf.converter.core.IURIResolver; import fr.opensagres.poi.xwpf.converter.core.ListItemContext; import fr.opensagres.poi.xwpf.converter.core.TableCellBorder; import fr.opensagres.poi.xwpf.converter.core.XWPFDocumentVisitor; import fr.opensagres.poi.xwpf.converter.core.styles.XWPFStylesDocument; import fr.opensagres.poi.xwpf.converter.core.styles.run.RunFontStyleStrikeValueProvider; import fr.opensagres.poi.xwpf.converter.core.styles.run.RunTextHighlightingValueProvider; import fr.opensagres.poi.xwpf.converter.core.utils.DxaUtil; import fr.opensagres.poi.xwpf.converter.core.utils.StringUtils; import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLOptions; import fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStyle; import fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStylePropertyConstants; import fr.opensagres.poi.xwpf.converter.xhtml.internal.styles.CSSStylesDocument; import fr.opensagres.poi.xwpf.converter.xhtml.internal.utils.SAXHelper; import fr.opensagres.poi.xwpf.converter.xhtml.internal.utils.StringEscapeUtils; public class XHTMLMapper extends XWPFDocumentVisitor<Object, XHTMLOptions, XHTMLMasterPage> { /** * There is no HTML representation for tab. So apply 4 spaces by default */ static final String TAB_CHAR_SEQUENCE = "    "; private static final String WORD_MEDIA = "word/media/"; private final ContentHandler contentHandler; /** * To hold paragraph reference and to be used while processing individual runs which has tabs */ private XWPFParagraph currentParagraph; private boolean generateStyles = true; private final IURIResolver resolver; private AttributesImpl currentRunAttributes; private boolean pageDiv; public XHTMLMapper( XWPFDocument document, ContentHandler contentHandler, XHTMLOptions options ) throws Exception { super( document, options != null ? options : XHTMLOptions.getDefault() ); this.contentHandler = contentHandler; this.resolver = getOptions().getURIResolver(); this.pageDiv = false; } @Override protected XWPFStylesDocument createStylesDocument( XWPFDocument document ) throws XmlException, IOException { return new CSSStylesDocument( document, options.isIgnoreStylesIfUnused(), options.getIndent() ); } @Override protected Object startVisitDocument() throws Exception { if ( !options.isFragment() ) { contentHandler.startDocument(); // html start startElement( HTML_ELEMENT ); // head start startElement( HEAD_ELEMENT ); if ( generateStyles ) { // styles ( (CSSStylesDocument) stylesDocument ).save( contentHandler ); } // html end endElement( HEAD_ELEMENT ); // body start startElement( BODY_ELEMENT ); } return null; } @Override protected void endVisitDocument() throws Exception { if ( pageDiv ) { endElement( DIV_ELEMENT ); } if ( !options.isFragment() ) { // body end endElement( BODY_ELEMENT ); // html end endElement( HTML_ELEMENT ); contentHandler.endDocument(); } } @Override protected Object startVisitSDT(XWPFSDT contents, Object container) throws SAXException { startElement(DIV_ELEMENT, null); // startElement(PRE_ELEMENT, null); return null; } // @Override // protected void visitSDTBody(XWPFSDT contents, Object sdtContainer) throws SAXException { // characters(contents.getContent().getText()); // } @Override protected void endVisitSDT(XWPFSDT contents, Object container, Object sdtContainer) throws SAXException { // endElement(PRE_ELEMENT); endElement(DIV_ELEMENT); } @Override protected Object startVisitParagraph( XWPFParagraph paragraph, ListItemContext itemContext, Object parentContainer ) throws Exception { // 1) create attributes // 1.1) Create "class" attributes. AttributesImpl attributes = createClassAttribute( paragraph.getStyleID() ); // 1.2) Create "style" attributes. CTPPr pPr = paragraph.getCTP().getPPr(); CSSStyle cssStyle = getStylesDocument().createCSSStyle( pPr ); if(cssStyle != null) { cssStyle.addProperty(CSSStylePropertyConstants.WHITE_SPACE, "pre-wrap"); } attributes = createStyleAttribute( cssStyle, attributes ); // 2) create element startElement( P_ELEMENT, attributes ); //To handle list items in paragraph if(itemContext != null) { startElement( SPAN_ELEMENT, attributes ); String text = itemContext.getText(); if ( StringUtils.isNotEmpty( text ) ) { text = StringUtils.replaceNonUnicodeChars(text); text = text + "\u0020"; SAXHelper.characters( contentHandler, StringEscapeUtils.escapeHtml( text ) ); } endElement( SPAN_ELEMENT ); } return null; } @Override protected void endVisitParagraph( XWPFParagraph paragraph, Object parentContainer, Object paragraphContainer ) throws Exception { endElement( P_ELEMENT ); } @Override protected void visitRun( XWPFRun run, boolean pageNumber, String url, Object paragraphContainer ) throws Exception { if(run.getParent() instanceof XWPFParagraph) { this.currentParagraph = (XWPFParagraph) run.getParent(); } XWPFParagraph paragraph = run.getParagraph(); // 1) create attributes // 1.1) Create "class" attributes. this.currentRunAttributes = createClassAttribute( paragraph.getStyleID() ); // 1.2) Create "style" attributes. CTRPr rPr = run.getCTR().getRPr(); CSSStyle cssStyle = getStylesDocument().createCSSStyle( rPr ); if (cssStyle != null) { cssStyle.addProperty(CSSStylePropertyConstants.WHITE_SPACE, "pre-wrap"); } this.currentRunAttributes = createStyleAttribute( cssStyle, currentRunAttributes ); if ( url != null ) { // url is not null, generate a HTML a. AttributesImpl hyperlinkAttributes = new AttributesImpl(); SAXHelper.addAttrValue( hyperlinkAttributes, HREF_ATTR, url ); startElement( A_ELEMENT, hyperlinkAttributes ); } super.visitRun( run, pageNumber, url, paragraphContainer ); if ( url != null ) { // url is not null, close the HTML a. // TODO : for the moment generate space to be ensure that a has some content. characters( " " ); endElement( A_ELEMENT ); } this.currentRunAttributes = null; this.currentParagraph = null; } @Override protected void visitEmptyRun( Object paragraphContainer ) throws Exception { startElement( BR_ELEMENT ); endElement( BR_ELEMENT ); } @Override protected void visitText( CTText ctText, boolean pageNumber, Object paragraphContainer ) throws Exception { if ( currentRunAttributes != null ) { startElement( SPAN_ELEMENT, currentRunAttributes ); } String text = ctText.getStringValue(); if ( StringUtils.isNotEmpty( text ) ) { // Escape with HTML characters characters( StringEscapeUtils.escapeHtml( text ) ); } // else // { // characters( SPACE_ENTITY ); // } if ( currentRunAttributes != null ) { endElement( SPAN_ELEMENT ); } } @Override protected void visitStyleText(XWPFRun run, String text) throws Exception { if(run.getFontFamily() == null) { run.setFontFamily(getStylesDocument().getFontFamilyAscii(run)); } if(run.getFontSize() <= 0) { run.setFontSize(getStylesDocument().getFontSize(run).intValue()); } CTRPr rPr = run.getCTR().getRPr(); // 1) create attributes // 1.1) Create "class" attributes. AttributesImpl runAttributes = createClassAttribute( currentParagraph.getStyleID() ); // 1.2) Create "style" attributes. CSSStyle cssStyle = getStylesDocument().createCSSStyle( rPr ); if(cssStyle != null) { Color color = RunTextHighlightingValueProvider.INSTANCE.getValue(rPr, getStylesDocument()); if(color != null) cssStyle.addProperty(CSSStylePropertyConstants.BACKGROUND_COLOR, StringUtils.toHexString(color)); if(Boolean.TRUE.equals(RunFontStyleStrikeValueProvider.INSTANCE.getValue(rPr, getStylesDocument())) || rPr.getDstrike() != null) cssStyle.addProperty("text-decoration", "line-through"); if(rPr.getVertAlign() != null) { int align = rPr.getVertAlign().getVal().intValue(); if(STVerticalAlignRun.INT_SUPERSCRIPT == align) { cssStyle.addProperty("vertical-align", "super"); } else if(STVerticalAlignRun.INT_SUBSCRIPT == align) { cssStyle.addProperty("vertical-align", "sub"); } } } runAttributes = createStyleAttribute( cssStyle, runAttributes ); if ( runAttributes != null ) { startElement( SPAN_ELEMENT, runAttributes ); } if ( StringUtils.isNotEmpty( text ) ) { // Escape with HTML characters characters( StringEscapeUtils.escapeHtml( text ) ); } if ( runAttributes != null ) { endElement( SPAN_ELEMENT ); } } @Override protected void visitTab( CTPTab o, Object paragraphContainer ) throws Exception { } @Override protected void visitTabs( CTTabs tabs, Object paragraphContainer ) throws Exception { //For some reason tabs become null ??? //Add equivalent spaces in html render as no tab in html world if(currentParagraph != null && tabs == null) { startElement( SPAN_ELEMENT, null ); characters(TAB_CHAR_SEQUENCE); endElement(SPAN_ELEMENT); return; } } @Override protected void addNewLine( CTBr br, Object paragraphContainer ) throws Exception { startElement( BR_ELEMENT ); endElement( BR_ELEMENT ); } @Override protected void pageBreak() throws Exception { } @Override protected void visitBookmark( CTBookmark bookmark, XWPFParagraph paragraph, Object paragraphContainer ) throws Exception { AttributesImpl attributes = new AttributesImpl(); SAXHelper.addAttrValue( attributes, ID_ATTR, bookmark.getName() ); startElement( SPAN_ELEMENT, attributes ); endElement( SPAN_ELEMENT ); } @Override protected Object startVisitTable( XWPFTable table, float[] colWidths, Object tableContainer ) throws Exception { // 1) create attributes // 1.1) Create class attributes. AttributesImpl attributes = createClassAttribute( table.getStyleID() ); // 1.2) Create "style" attributes. CTTblPr tblPr = table.getCTTbl().getTblPr(); CSSStyle cssStyle = getStylesDocument().createCSSStyle( tblPr ); if(cssStyle != null) { cssStyle.addProperty(CSSStylePropertyConstants.BORDER_COLLAPSE, CSSStylePropertyConstants.BORDER_COLLAPSE_COLLAPSE); } attributes = createStyleAttribute( cssStyle, attributes ); // 2) create element startElement( TABLE_ELEMENT, attributes ); return null; } @Override protected void endVisitTable( XWPFTable table, Object parentContainer, Object tableContainer ) throws Exception { endElement( TABLE_ELEMENT ); } @Override protected void startVisitTableRow( XWPFTableRow row, Object tableContainer, int rowIndex, boolean headerRow ) throws Exception { // 1) create attributes // Create class attributes. XWPFTable table = row.getTable(); AttributesImpl attributes = createClassAttribute( table.getStyleID() ); // 2) create element if ( headerRow ) { startElement( TH_ELEMENT, attributes ); } else { startElement( TR_ELEMENT, attributes ); } } @Override protected void endVisitTableRow( XWPFTableRow row, Object tableContainer, boolean firstRow, boolean lastRow, boolean headerRow ) throws Exception { if ( headerRow ) { endElement( TH_ELEMENT ); } else { endElement( TR_ELEMENT ); } } @Override protected Object startVisitTableCell( XWPFTableCell cell, Object tableContainer, boolean firstRow, boolean lastRow, boolean firstCell, boolean lastCell, List<XWPFTableCell> vMergeCells ) throws Exception { // 1) create attributes // 1.1) Create class attributes. XWPFTableRow row = cell.getTableRow(); XWPFTable table = row.getTable(); AttributesImpl attributes = createClassAttribute( table.getStyleID() ); // 1.2) Create "style" attributes. CTTcPr tcPr = cell.getCTTc().getTcPr(); CSSStyle cssStyle = getStylesDocument().createCSSStyle( tcPr ); //At lease support solid borders for now if(cssStyle != null) { TableCellBorder border = getStylesDocument().getTableBorder(table, BorderSide.TOP); if(border != null) { String style = border.getBorderSize() + "px solid " +StringUtils.toHexString(border.getBorderColor()); cssStyle.addProperty(CSSStylePropertyConstants.BORDER_TOP, style); } border = getStylesDocument().getTableBorder(table, BorderSide.BOTTOM); if(border != null) { String style = border.getBorderSize() + "px solid " + StringUtils.toHexString(border.getBorderColor()); cssStyle.addProperty(CSSStylePropertyConstants.BORDER_BOTTOM, style); } border = getStylesDocument().getTableBorder(table, BorderSide.LEFT); if(border != null) { String style = border.getBorderSize() + "px solid " + StringUtils.toHexString(border.getBorderColor()); cssStyle.addProperty(CSSStylePropertyConstants.BORDER_LEFT, style); } border = getStylesDocument().getTableBorder(table, BorderSide.RIGHT); if(border != null) { String style = border.getBorderSize() + "px solid " + StringUtils.toHexString(border.getBorderColor()); cssStyle.addProperty(CSSStylePropertyConstants.BORDER_RIGHT, style); } } attributes = createStyleAttribute( cssStyle, attributes ); // colspan attribute BigInteger gridSpan = stylesDocument.getTableCellGridSpan( cell ); if ( gridSpan != null ) { attributes = SAXHelper.addAttrValue( attributes, COLSPAN_ATTR, gridSpan.intValue() ); } if ( vMergeCells != null ) { attributes = SAXHelper.addAttrValue( attributes, ROWSPAN_ATTR, vMergeCells.size() ); } // 2) create element startElement( TD_ELEMENT, attributes ); return null; } @Override protected void endVisitTableCell( XWPFTableCell cell, Object tableContainer, Object tableCellContainer ) throws Exception { endElement( TD_ELEMENT ); } @Override protected void visitHeader( XWPFHeader header, CTHdrFtrRef headerRef, CTSectPr sectPr, XHTMLMasterPage masterPage ) throws Exception { // TODO Auto-generated method stub } @Override protected void visitFooter( XWPFFooter footer, CTHdrFtrRef footerRef, CTSectPr sectPr, XHTMLMasterPage masterPage ) throws Exception { // TODO Auto-generated method stub } @Override protected void visitPicture( CTPicture picture, Float offsetX, Enum relativeFromH, Float offsetY, org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STRelFromV.Enum relativeFromV, org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.STWrapText.Enum wrapText, Object parentContainer ) throws Exception { AttributesImpl attributes = null; // Src attribute XWPFPictureData pictureData = super.getPictureData( picture ); if ( pictureData != null ) { // img/@src String src = pictureData.getFileName(); if ( StringUtils.isNotEmpty( src ) ) { src = resolver.resolve( WORD_MEDIA + src ); attributes = SAXHelper.addAttrValue( attributes, SRC_ATTR, src ); } CTPositiveSize2D ext = picture.getSpPr().getXfrm().getExt(); CSSStyle style = new CSSStyle( IMG_ELEMENT, null ); // img/@width float width = emu2points( ext.getCx() ); // img/@height float height = emu2points( ext.getCy() ); style.addProperty(WIDTH, getStylesDocument().getValueAsPoint( width ) ); style.addProperty(HEIGHT, getStylesDocument().getValueAsPoint( height ) ); attributes = SAXHelper.addAttrValue( attributes, STYLE_ATTR, style.getInlineStyles() ); } else { // external link images inserted String link = picture.getBlipFill().getBlip().getLink(); String src = document.getPackagePart().getRelationships().getRelationshipByID(link).getTargetURI().toString(); attributes = SAXHelper.addAttrValue( null, SRC_ATTR, src ); CTPositiveSize2D ext = picture.getSpPr().getXfrm().getExt(); CSSStyle style = new CSSStyle( IMG_ELEMENT, null ); // img/@width float width = emu2points( ext.getCx() ); // img/@height float height = emu2points( ext.getCy() ); style.addProperty(WIDTH, getStylesDocument().getValueAsPoint( width ) ); style.addProperty(HEIGHT, getStylesDocument().getValueAsPoint( height ) ); attributes = SAXHelper.addAttrValue( attributes, STYLE_ATTR, style.getInlineStyles() ); } if ( attributes != null ) { startElement( IMG_ELEMENT, attributes ); endElement( IMG_ELEMENT ); } } public void setActiveMasterPage( XHTMLMasterPage masterPage ) { if ( pageDiv ) { try { endElement( DIV_ELEMENT ); } catch ( SAXException e ) { e.printStackTrace(); } } AttributesImpl attributes = new AttributesImpl(); CSSStyle style = new CSSStyle( DIV_ELEMENT, null ); CTSectPr sectPr = masterPage.getSectPr(); CTPageSz pageSize = sectPr.getPgSz(); if ( pageSize != null ) { // Width BigInteger width = pageSize.getW(); if ( width != null ) { style.addProperty( WIDTH, getStylesDocument().getValueAsPoint( DxaUtil.dxa2points( width ) ) ); } } CTPageMar pageMargin = sectPr.getPgMar(); if ( pageMargin != null ) { // margin bottom BigInteger marginBottom = pageMargin.getBottom(); if ( marginBottom != null ) { float marginBottomPt = DxaUtil.dxa2points( marginBottom ); style.addProperty( MARGIN_BOTTOM, getStylesDocument().getValueAsPoint( marginBottomPt ) ); } // margin top BigInteger marginTop = pageMargin.getTop(); if ( marginTop != null ) { float marginTopPt = DxaUtil.dxa2points( marginTop ); style.addProperty( MARGIN_TOP, getStylesDocument().getValueAsPoint( marginTopPt ) ); } // margin left BigInteger marginLeft = pageMargin.getLeft(); if ( marginLeft != null ) { float marginLeftPt = DxaUtil.dxa2points( marginLeft ); style.addProperty( MARGIN_LEFT, getStylesDocument().getValueAsPoint( marginLeftPt ) ); } // margin right BigInteger marginRight = pageMargin.getRight(); if ( marginRight != null ) { float marginRightPt = DxaUtil.dxa2points( marginRight ); style.addProperty( MARGIN_RIGHT, getStylesDocument().getValueAsPoint( marginRightPt ) ); } } String s = style.getInlineStyles(); if ( StringUtils.isNotEmpty( s ) ) { SAXHelper.addAttrValue( attributes, STYLE_ATTR, s ); } try { startElement( DIV_ELEMENT, attributes ); } catch ( SAXException e ) { // TODO Auto-generated catch block e.printStackTrace(); } pageDiv = true; } public XHTMLMasterPage createMasterPage( CTSectPr sectPr ) { return new XHTMLMasterPage( sectPr ); } private void startElement( String name ) throws SAXException { startElement( name, null ); } private void startElement( String name, Attributes attributes ) throws SAXException { SAXHelper.startElement( contentHandler, name, attributes ); } private void endElement( String name ) throws SAXException { SAXHelper.endElement( contentHandler, name ); } private void characters( String content ) throws SAXException { SAXHelper.characters( contentHandler, content ); } @Override public CSSStylesDocument getStylesDocument() { return (CSSStylesDocument) super.getStylesDocument(); } private AttributesImpl createClassAttribute( String styleID ) { String classNames = getStylesDocument().getClassNames( styleID ); if ( StringUtils.isNotEmpty( classNames ) ) { return SAXHelper.addAttrValue( null, CLASS_ATTR, classNames ); } return null; } private AttributesImpl createStyleAttribute( CSSStyle cssStyle, AttributesImpl attributes ) { if ( cssStyle != null ) { String inlineStyles = cssStyle.getInlineStyles(); if ( StringUtils.isNotEmpty( inlineStyles ) ) { attributes = SAXHelper.addAttrValue( attributes, STYLE_ATTR, inlineStyles ); } } return attributes; } }