Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ arbitrary XML, JSON, etc.</description>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.11.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ public final class DocxConstants {
public static final QName QNAME_VERTICAL_ALIGNMENT_ATT = new QName("", "vertical-alignment");
public static final QName QNAME_WIDTH_ATT = new QName("", "width");
public static final QName QNAME_XSLT_FORMAT_ATT = new QName("", "xslt-format");
public static final QName QNAME_NUMID_ATT = new QName("", "numId");


// Elements:
public static final QName QNAME_COLS_ELEM = new QName(SIMPLE_WP_NS, "cols");
Expand Down
221 changes: 209 additions & 12 deletions src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import javax.imageio.ImageIO;
import javax.xml.namespace.QName;
Expand All @@ -35,6 +38,7 @@
import org.apache.poi.wp.usermodel.HeaderFooterType;
import org.apache.poi.xwpf.usermodel.BreakType;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
import org.apache.poi.xwpf.usermodel.TableWidthType;
import org.apache.poi.xwpf.usermodel.UnderlinePatterns;
import org.apache.poi.xwpf.usermodel.XWPFAbstractFootnoteEndnote;
import org.apache.poi.xwpf.usermodel.XWPFAbstractNum;
Expand All @@ -46,6 +50,7 @@
import org.apache.poi.xwpf.usermodel.XWPFNum;
import org.apache.poi.xwpf.usermodel.XWPFNumbering;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRelation;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFStyle;
import org.apache.poi.xwpf.usermodel.XWPFStyles;
Expand All @@ -71,6 +76,8 @@
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHdrFtrRef;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHyperlink;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTMarkupRange;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumLvl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTOnOff;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar;
Expand All @@ -84,6 +91,8 @@
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSimpleField;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTStyle;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGrid;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblGridCol;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblLayoutType;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTblWidth;
Expand Down Expand Up @@ -377,6 +386,7 @@ public boolean hasBorders() {
}

private static final Logger log = LogManager.getLogger(DocxGenerator.class);
private static final String DEFAULT_TABLE_WIDTH = "100%";

private File outFile;
private int dotsPerInch = 72; /* DPI */
Expand All @@ -390,6 +400,8 @@ public boolean hasBorders() {
private boolean isFirstParagraphStyleWarning = true;
private boolean isFirstCharacterStyleWarning = true;
private boolean isFirstTableStyleWarning = true;
private int maxNum;
private Map<String, List<XWPFParagraph>> numParas = new LinkedHashMap<>();

/**
*
Expand All @@ -416,8 +428,8 @@ public void generate(XmlObject xml) throws DocxGenerationException, XmlException
setupNumbering(doc, this.templateDoc);
setupStyles(doc, this.templateDoc);
constructDoc(doc, xml);

FileOutputStream out = new FileOutputStream(outFile);
setNumbering(doc);
FileOutputStream out = new FileOutputStream(this.outFile);
doc.write(out);
doc.close();
}
Expand Down Expand Up @@ -1230,6 +1242,7 @@ private XWPFParagraph makeParagraph(
cursor.push();
String styleName = cursor.getAttributeText(DocxConstants.QNAME_STYLE_ATT);
String styleId = cursor.getAttributeText(DocxConstants.QNAME_STYLEID_ATT);
String numId = cursor.getAttributeText(DocxConstants.QNAME_NUMID_ATT);

if (null != styleName && null == styleId) {
// Look up the style by name:
Expand Down Expand Up @@ -1328,6 +1341,13 @@ private XWPFParagraph makeParagraph(
} while(cursor.toNextSibling());
}
cursor.pop();

if (numId != null && !numId.isEmpty()) {
// Store Numbering paragraph
List<XWPFParagraph> paras = this.numParas.computeIfAbsent(numId, k -> new ArrayList<>());
paras.add(para);
}

return para;
}

Expand Down Expand Up @@ -2154,23 +2174,39 @@ private void makeHyperlink(XWPFParagraph para, XmlCursor cursor) throws DocxGene

// Set the appropriate target:

XWPFHyperlinkRun hyperlinkRun;

if (href.startsWith("#")) {
// Just a fragment ID, must be to a bookmark
String bookmarkName = href.substring(1);
hyperlink.setAnchor(bookmarkName);
cursor.push();
hyperlinkRun = makeHyperlinkRun(hyperlink, cursor, para);
cursor.pop();
} else {
// Create a relationship that targets the href and use the
// relationship's ID on the hyperlink
// It's not yet clear from the POI API how to create a new relationship for
// use by an external hyperlink.
// throw new NotImplementedException("Links to external resources not yet implemented.");
}
// Add the link as External relationship
String id = para.getDocument().getPackagePart().addExternalRelationship(href, XWPFRelation.HYPERLINK.getRelation()).getId();

cursor.push();
XWPFHyperlinkRun hyperlinkRun = makeHyperlinkRun(hyperlink, cursor, para);
cursor.pop();
para.addRun(hyperlinkRun);
// Append the link and bind it to the relationship
hyperlink.setId(id);

// Create the linked text
String linkedText = cursor.getTextValue();
CTText ctText = CTText.Factory.newInstance();
ctText.setStringValue(linkedText);
CTR ctr = CTR.Factory.newInstance();
ctr.setTArray(new CTText[]{ctText});

// Create the formatting
CTRPr rpr = ctr.addNewRPr();
rpr.addNewRStyle().setVal("Hyperlink");

// Insert the linked text into the link
hyperlink.setRArray(new CTR[]{ctr});

hyperlinkRun = new XWPFHyperlinkRun(hyperlink, CTR.Factory.newInstance(), para);
}
para.addRun(hyperlinkRun);
}

/**
Expand Down Expand Up @@ -2323,6 +2359,9 @@ private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationExce
} while (cursor.toNextSibling());
}

setDefaultTableWidthIfNeeded(table);
addTableGridWithColumnsIfNeeded(table, colDefs);

// populate the rows and cells.
cursor = xml.newCursor();

Expand Down Expand Up @@ -2992,6 +3031,26 @@ private void setupStyles(XWPFDocument doc, XWPFDocument templateDoc) throws Docx

}

private void setNumbering(XWPFDocument doc) {
int i = 1;

for (List<XWPFParagraph> paras : this.numParas.values()) {

int newNum = this.maxNum + i;
BigInteger bgiNumId = BigInteger.valueOf(newNum);
for (XWPFParagraph numPara : paras) {
CTPPr ctpPr = numPara.getCTPPr();
CTNumPr ctNumPr = ctpPr.addNewNumPr();
CTDecimalNumber decNumId = CTDecimalNumber.Factory.newInstance();
decNumId.setVal(bgiNumId);
ctNumPr.setNumId(decNumId);
}
linkListNumIdToStyleAbstractIdAndRestartListLevels(doc, paras, bgiNumId);

i++;
}
}

private void setupNumbering(XWPFDocument doc, XWPFDocument templateDoc) throws DocxGenerationException {
// Load the template's numbering definitions to the new document

Expand Down Expand Up @@ -3028,6 +3087,10 @@ private void setupNumbering(XWPFDocument doc, XWPFDocument templateDoc) throws D
} while (num != null);


// Calculate max numbering number
Optional<XWPFNum> maxXwpfNum = numbering.getNums().stream().max(Comparator.comparing(o -> o.getCTNum().getNumId()));
this.maxNum = maxXwpfNum.map(xwpfNum -> xwpfNum.getCTNum().getNumId().intValue()).orElse(0);

} catch (Exception e) {
new DocxGenerationException(e.getClass().getSimpleName() + " Copying numbering definitions from template doc: " + e.getMessage(), e);
}
Expand Down Expand Up @@ -3114,5 +3177,139 @@ else if ("jpeg".equals(imgExtension) ||
return format;
}

/**
* <p>
* Add table grid (w:tblGrid) and grid columns (w:gridCol) based on table column definitions.
* </p>
*
* @param table XWPF table
* @param colDefs table column definitions
*/
public static void addTableGridWithColumnsIfNeeded(XWPFTable table, TableColumnDefinitions colDefs) {
setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs);
CTTblGrid tblGrid = table.getCTTbl().getTblGrid();
if (tblGrid == null) {
tblGrid = table.getCTTbl().addNewTblGrid();
for (TableColumnDefinition colDef : colDefs.getColumnDefinitions()) {
String specifiedWidth = colDef.getSpecifiedWidth();
CTTblGridCol gridCol = tblGrid.addNewGridCol();
BigInteger gridColWidth;
// logic below has been copied from XWPFTable.setWidthValue
if (specifiedWidth.matches(XWPFTable.REGEX_PERCENTAGE)) {
String numberPart = specifiedWidth.substring(0, specifiedWidth.length() - 1);
double percentage = Double.parseDouble(numberPart) * 50;
long intValue = Math.round(percentage);
gridColWidth = BigInteger.valueOf(intValue);
} else if (specifiedWidth.matches("auto")) {
gridColWidth = BigInteger.ZERO;
} else {
gridColWidth = new BigInteger(specifiedWidth);
}
gridCol.setW(gridColWidth);
}
}
}

/**
* <p>
* Set table width to 100% and change width type if needed for correct displaying in the both MS Word and LibreOffice
* </p>
*
* @param table XWPF table
*/
public static void setDefaultTableWidthIfNeeded(XWPFTable table) {
if (table.getWidthType() == TableWidthType.AUTO && table.getWidth() == 0) {
table.setWidth(DEFAULT_TABLE_WIDTH);
} else if (table.getWidthType() == TableWidthType.NIL) {
table.setWidthType(TableWidthType.PCT);
table.setWidth(DEFAULT_TABLE_WIDTH);
}
}

/**
* <p>
* Set columns width in percentages depending on columns count, if all columns have auto width
* </p>
*
* @param colDefs table column definitions
*/
public static void setColumnsWidthInPercentagesIfAllHaveAutoWidth(TableColumnDefinitions colDefs) {
if (colDefs.getColumnDefinitions().isEmpty()) {
return;
}
boolean autoWidth = true;
for (TableColumnDefinition colDef : colDefs.getColumnDefinitions()) {
String specifiedWidth = colDef.getSpecifiedWidth();
if (!"auto".equalsIgnoreCase(specifiedWidth)) {
autoWidth = false;
break;
}
}
if (autoWidth) {
final int columnsCount = colDefs.getColumnDefinitions().size();
final int columnWidth = 100 / columnsCount;
final int dotsPerInch = 72;
for (TableColumnDefinition colDef : colDefs.getColumnDefinitions()) {
try {
colDef.setWidth(columnWidth + "%", dotsPerInch);
} catch (MeasurementException e) {
throw new RuntimeException("Error setting column width: " + columnWidth, e);
}
}
}
}

/**
* <p>
* Link list numId to style abstractNumId and restart list items ordering from 1
* </p>
* <p>
* Assume all paras have the same style
* </p>
*
* @param doc XWPF document
* @param paras list items
* @param numId list numId
*/
public static void linkListNumIdToStyleAbstractIdAndRestartListLevels(XWPFDocument doc, List<XWPFParagraph> paras, BigInteger numId) {
String styleId = paras.get(0).getStyle();
XWPFStyle style = doc.getStyles().getStyle(styleId);
if (style != null) {
linkNumIdToStyleAbstractId(style, doc, numId);
} else {
doc.getNumbering().addNum(numId);
}
addLevelOverrideFromOneToNumId(doc, numId);
}

/**
* <p>
* Link numId to style abstractNumId
* </p>
*
* @param style XWPF style
* @param doc XWPF document
* @param numId list numId
*/
private static void linkNumIdToStyleAbstractId(XWPFStyle style, XWPFDocument doc, BigInteger numId) {
BigInteger styleNumId = style.getCTStyle().getPPr().getNumPr().getNumId().getVal();
BigInteger abstractNumId = doc.getNumbering().getNum(styleNumId).getCTNum().getAbstractNumId().getVal();
doc.getNumbering().addNum(abstractNumId, numId);
}

/**
* <p>
* Add level override from 1 to numId
* </p>
*
* @param doc XWPF document
* @param numId list numId
*/
private static void addLevelOverrideFromOneToNumId(XWPFDocument doc, BigInteger numId) {
CTNumLvl numLvl = doc.getNumbering().getNum(numId).getCTNum().addNewLvlOverride();
numLvl.setIlvl(BigInteger.ZERO);
CTDecimalNumber startOverride = numLvl.addNewStartOverride();
startOverride.setVal(BigInteger.ONE);
}

}
6 changes: 2 additions & 4 deletions src/main/xsl/html2docx/baseProcessing.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,8 @@

<xsl:template match="xhtml:a[@href] | a[@href]">
<xsl:param name="doDebug" as="xs:boolean" tunnel="yes" select="false()"/>

<xsl:variable name="targetId" as="xs:string?" select="tokenize(@href, '#')[last()]"/>

<wp:hyperlink href="#{$targetId}">
<xsl:variable name="targetId" as="xs:string?" select="@href"/>
<wp:hyperlink href="{$targetId}">
<xsl:apply-templates>
<xsl:with-param name="doDebug" as="xs:boolean" tunnel="yes" select="$doDebug"/>
</xsl:apply-templates>
Expand Down
9 changes: 8 additions & 1 deletion src/main/xsl/html2docx/get-style-name.xsl
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@
<xsl:variable name="level" as="xs:string"
select="if ($level eq '1') then '' else $level"
/>
<xsl:sequence select="'List ' || $level"/>
<xsl:choose>
<xsl:when test="$list-type = 'ol'">
<xsl:sequence select="'List Number ' || $level"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="'List ' || $level"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

<xsl:template mode="get-style-name" match="*" priority="-1" as="xs:string?">
Expand Down
Loading