From 053c9d9abebfb454a81894b67eb2d4b38a21b073 Mon Sep 17 00:00:00 2001 From: Oleksandr Oboznyi Date: Wed, 8 Nov 2023 11:57:59 +0200 Subject: [PATCH 1/6] Fix external hyperlinks, fix ordered lists --- .../xml2docx/generator/DocxGenerator.java | 37 ++++++++++++++----- src/main/xsl/html2docx/baseProcessing.xsl | 6 +-- src/main/xsl/html2docx/get-style-name.xsl | 9 ++++- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java index ba2e990..3c0560e 100644 --- a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java +++ b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java @@ -46,6 +46,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; @@ -2154,23 +2155,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); } /** diff --git a/src/main/xsl/html2docx/baseProcessing.xsl b/src/main/xsl/html2docx/baseProcessing.xsl index 22de534..c1ba5e7 100644 --- a/src/main/xsl/html2docx/baseProcessing.xsl +++ b/src/main/xsl/html2docx/baseProcessing.xsl @@ -141,10 +141,8 @@ - - - - + + diff --git a/src/main/xsl/html2docx/get-style-name.xsl b/src/main/xsl/html2docx/get-style-name.xsl index 54be602..2fa7f1b 100644 --- a/src/main/xsl/html2docx/get-style-name.xsl +++ b/src/main/xsl/html2docx/get-style-name.xsl @@ -87,7 +87,14 @@ - + + + + + + + + From d1fa79932ce7756f4912d3daa4ca0a113538bedc Mon Sep 17 00:00:00 2001 From: Oleksandr Oboznyi Date: Wed, 8 Nov 2023 13:05:42 +0200 Subject: [PATCH 2/6] Set different numId to each ordered list paragraph to avoid continuous numbering --- .../xml2docx/generator/DocxConstants.java | 2 + .../xml2docx/generator/DocxGenerator.java | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/wordinator/xml2docx/generator/DocxConstants.java b/src/main/java/org/wordinator/xml2docx/generator/DocxConstants.java index ef3f9e6..3764597 100644 --- a/src/main/java/org/wordinator/xml2docx/generator/DocxConstants.java +++ b/src/main/java/org/wordinator/xml2docx/generator/DocxConstants.java @@ -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"); diff --git a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java index 3c0560e..9c71b49 100644 --- a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java +++ b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java @@ -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; @@ -72,6 +75,7 @@ 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.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; @@ -391,6 +395,8 @@ public boolean hasBorders() { private boolean isFirstParagraphStyleWarning = true; private boolean isFirstCharacterStyleWarning = true; private boolean isFirstTableStyleWarning = true; + private int maxNum; + private Map> numParas = new LinkedHashMap<>(); /** * @@ -417,8 +423,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(); } @@ -1231,6 +1237,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: @@ -1329,6 +1336,13 @@ private XWPFParagraph makeParagraph( } while(cursor.toNextSibling()); } cursor.pop(); + + if (numId != null && !numId.isEmpty()) { + // Store Numbering paragraph + List paras = this.numParas.computeIfAbsent(numId, k -> new ArrayList<>()); + paras.add(para); + } + return para; } @@ -3009,6 +3023,26 @@ private void setupStyles(XWPFDocument doc, XWPFDocument templateDoc) throws Docx } + private void setNumbering(XWPFDocument doc) { + int i = 1; + + for (List 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); + } + doc.getNumbering().addNum(bgiNumId); + + i++; + } + } + private void setupNumbering(XWPFDocument doc, XWPFDocument templateDoc) throws DocxGenerationException { // Load the template's numbering definitions to the new document @@ -3045,6 +3079,10 @@ private void setupNumbering(XWPFDocument doc, XWPFDocument templateDoc) throws D } while (num != null); + // Calculate max numbering number + Optional 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); } From bd802d1e5053cead41109b3f0da99efe6f2a77ae Mon Sep 17 00:00:00 2001 From: Oleksandr Oboznyi Date: Wed, 8 Nov 2023 14:00:13 +0200 Subject: [PATCH 3/6] Column width corresponds defined in 'col' tag --- pom.xml | 6 ++ .../xml2docx/generator/DocxGenerator.java | 54 ++++++++++ .../xml2docx/TestDocxGenerator.java | 99 +++++++++++++++++++ 3 files changed, 159 insertions(+) diff --git a/pom.xml b/pom.xml index 97f5f00..f57fe8c 100644 --- a/pom.xml +++ b/pom.xml @@ -54,6 +54,12 @@ arbitrary XML, JSON, etc. 4.13.2 test + + org.mockito + mockito-core + 4.11.0 + test + org.apache.poi poi diff --git a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java index 9c71b49..5c1e038 100644 --- a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java +++ b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java @@ -38,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; @@ -89,6 +90,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; @@ -382,6 +385,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 */ @@ -2354,6 +2358,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(); @@ -3169,5 +3176,52 @@ else if ("jpeg".equals(imgExtension) || return format; } + /** + *

+ * Add table grid (w:tblGrid) and grid columns (w:gridCol) based on table column definitions. + *

+ * + * @param table XWPF table + * @param colDefs table column definitions + */ + public static void addTableGridWithColumnsIfNeeded(XWPFTable table, TableColumnDefinitions 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); + } + } + } + + /** + *

+ * Set table width to 100% and change width type if needed for correct displaying in the both MS Word and LibreOffice + *

+ * + * @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); + } + } } diff --git a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java index c21a579..7cecbb5 100644 --- a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java +++ b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java @@ -10,6 +10,7 @@ import org.apache.poi.xwpf.usermodel.BodyElementType; import org.apache.poi.xwpf.usermodel.IBodyElement; import org.apache.poi.xwpf.usermodel.IRunElement; +import org.apache.poi.xwpf.usermodel.TableWidthType; import org.apache.poi.xwpf.usermodel.XWPFAbstractNum; import org.apache.poi.xwpf.usermodel.XWPFDocument; import org.apache.poi.xwpf.usermodel.XWPFFooter; @@ -26,16 +27,22 @@ import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlObject; import org.junit.Test; +import org.mockito.Mockito; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocument1; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr; +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.CTText; import org.openxmlformats.schemas.wordprocessingml.x2006.main.STFldCharType; import org.wordinator.xml2docx.generator.DocxConstants; import org.wordinator.xml2docx.generator.DocxGenerator; +import org.wordinator.xml2docx.generator.MeasurementException; +import org.wordinator.xml2docx.generator.TableColumnDefinitions; import junit.framework.TestCase; @@ -515,6 +522,98 @@ public void testNestedTableWidth() throws Exception { assertEquals(BodyElementType.TABLE, elem.getElementType()); } + @Test + public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_width_in_percentages() throws MeasurementException { + // GIVEN + XWPFTable table = Mockito.mock(XWPFTable.class); + CTTbl ctTbl = Mockito.mock(CTTbl.class); + CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class); + CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class); + CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class); + Mockito.when(table.getCTTbl()).thenReturn(ctTbl); + Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid); + Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2); + + TableColumnDefinitions colDefs = new TableColumnDefinitions(); + final int dotsPerInch = 72; + colDefs.newColumnDef().setWidth("30%", dotsPerInch); + colDefs.newColumnDef().setWidth("70%", dotsPerInch); + + // WHEN + DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs); + + // THEN + Mockito.verify(col1).setW(BigInteger.valueOf(1500)); + Mockito.verify(col2).setW(BigInteger.valueOf(3500)); + } + + @Test + public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_auto_width() { + // GIVEN + XWPFTable table = Mockito.mock(XWPFTable.class); + CTTbl ctTbl = Mockito.mock(CTTbl.class); + CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class); + CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class); + CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class); + Mockito.when(table.getCTTbl()).thenReturn(ctTbl); + Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid); + Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2); + + TableColumnDefinitions colDefs = new TableColumnDefinitions(); + colDefs.newColumnDef().setWidthAuto(); + colDefs.newColumnDef().setWidthAuto(); + + // WHEN + DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs); + + // THEN + Mockito.verify(col1).setW(BigInteger.ZERO); + Mockito.verify(col2).setW(BigInteger.ZERO); + } + + @Test + public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_width_in_ints() throws MeasurementException { + // GIVEN + XWPFTable table = Mockito.mock(XWPFTable.class); + CTTbl ctTbl = Mockito.mock(CTTbl.class); + CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class); + CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class); + CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class); + Mockito.when(table.getCTTbl()).thenReturn(ctTbl); + Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid); + Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2); + + TableColumnDefinitions colDefs = new TableColumnDefinitions(); + final int dotsPerInch = 72; + colDefs.newColumnDef().setWidth("30", dotsPerInch); + colDefs.newColumnDef().setWidth("70", dotsPerInch); + + // WHEN + DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs); + + // THEN + Mockito.verify(col1).setW(BigInteger.valueOf(30)); + Mockito.verify(col2).setW(BigInteger.valueOf(70)); + } + + @Test + public void testSetDefaultTableWidthIfNeeded__should_set_100_percentages_width_for_auto_width_type() { + XWPFTable table = Mockito.mock(XWPFTable.class); + Mockito.when(table.getWidthType()).thenReturn(TableWidthType.AUTO); + Mockito.when(table.getWidth()).thenReturn(0); + DocxGenerator.setDefaultTableWidthIfNeeded(table); + Mockito.verify(table).setWidth("100%"); + } + + @Test + public void testSetDefaultTableWidthIfNeeded__should_set_100_percentages_width_and_pct_type_for_nil_width_type() { + XWPFTable table = Mockito.mock(XWPFTable.class); + Mockito.when(table.getWidthType()).thenReturn(TableWidthType.NIL); + DocxGenerator.setDefaultTableWidthIfNeeded(table); + Mockito.verify(table).setWidthType(TableWidthType.PCT); + Mockito.verify(table).setWidth("100%"); + } + // ===== INTERNAL UTILITIES private XWPFDocument convert(String infile, String outfile) throws Exception { From 5bc519db8ea224c1555f17a05c4a29e8bd3ebba7 Mon Sep 17 00:00:00 2001 From: Oleksandr Oboznyi Date: Wed, 8 Nov 2023 15:22:32 +0200 Subject: [PATCH 4/6] Set columns width in percentages if all column definitions has "colwidth" = "auto --- .../xml2docx/generator/DocxGenerator.java | 34 ++++++++ .../xml2docx/TestDocxGenerator.java | 84 +++++++++++++++++-- 2 files changed, 110 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java index 5c1e038..9ed8079 100644 --- a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java +++ b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java @@ -3185,6 +3185,7 @@ else if ("jpeg".equals(imgExtension) || * @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(); @@ -3224,4 +3225,37 @@ public static void setDefaultTableWidthIfNeeded(XWPFTable table) { } } + /** + *

+ * Set columns width in percentages depending on columns count, if all columns have auto width + *

+ * + * @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); + } + } + } + } + } diff --git a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java index 7cecbb5..a93ab46 100644 --- a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java +++ b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java @@ -26,6 +26,7 @@ import org.apache.poi.xwpf.usermodel.XWPFTableRow; import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlObject; +import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody; @@ -49,6 +50,7 @@ public class TestDocxGenerator extends TestCase { public static final String DOTX_TEMPLATE_PATH = "docx/Test_Template.dotx"; + private static final int DOTS_PER_INCH = 72; @Test public void testMakeDocx() throws Exception { @@ -535,9 +537,8 @@ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_width_ Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2); TableColumnDefinitions colDefs = new TableColumnDefinitions(); - final int dotsPerInch = 72; - colDefs.newColumnDef().setWidth("30%", dotsPerInch); - colDefs.newColumnDef().setWidth("70%", dotsPerInch); + colDefs.newColumnDef().setWidth("30%", DOTS_PER_INCH); + colDefs.newColumnDef().setWidth("70%", DOTS_PER_INCH); // WHEN DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs); @@ -547,6 +548,30 @@ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_width_ Mockito.verify(col2).setW(BigInteger.valueOf(3500)); } + @Test + public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_mixed_width() throws MeasurementException { + // GIVEN + XWPFTable table = Mockito.mock(XWPFTable.class); + CTTbl ctTbl = Mockito.mock(CTTbl.class); + CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class); + CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class); + CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class); + Mockito.when(table.getCTTbl()).thenReturn(ctTbl); + Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid); + Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2); + + TableColumnDefinitions colDefs = new TableColumnDefinitions(); + colDefs.newColumnDef().setWidth("30%", DOTS_PER_INCH); + colDefs.newColumnDef().setWidthAuto(); + + // WHEN + DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs); + + // THEN + Mockito.verify(col1).setW(BigInteger.valueOf(1500)); + Mockito.verify(col2).setW(BigInteger.ZERO); + } + @Test public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_auto_width() { // GIVEN @@ -567,8 +592,8 @@ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_auto_w DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs); // THEN - Mockito.verify(col1).setW(BigInteger.ZERO); - Mockito.verify(col2).setW(BigInteger.ZERO); + Mockito.verify(col1).setW(BigInteger.valueOf(2500)); + Mockito.verify(col2).setW(BigInteger.valueOf(2500)); } @Test @@ -584,9 +609,8 @@ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_width_ Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2); TableColumnDefinitions colDefs = new TableColumnDefinitions(); - final int dotsPerInch = 72; - colDefs.newColumnDef().setWidth("30", dotsPerInch); - colDefs.newColumnDef().setWidth("70", dotsPerInch); + colDefs.newColumnDef().setWidth("30", DOTS_PER_INCH); + colDefs.newColumnDef().setWidth("70", DOTS_PER_INCH); // WHEN DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs); @@ -614,6 +638,50 @@ public void testSetDefaultTableWidthIfNeeded__should_set_100_percentages_width_a Mockito.verify(table).setWidth("100%"); } + @Test + public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_set_33_percentages_width() { + // GIVEN + TableColumnDefinitions colDefs = new TableColumnDefinitions(); + colDefs.newColumnDef().setWidthAuto(); + colDefs.newColumnDef().setWidthAuto(); + colDefs.newColumnDef().setWidthAuto(); + + // WHEN + DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs); + + // THEN + Assert.assertEquals("33%", colDefs.get(0).getWidth()); + Assert.assertEquals("33%", colDefs.get(1).getWidth()); + Assert.assertEquals("33%", colDefs.get(2).getWidth()); + } + + @Test + public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_not_change_width_empty_definitions() { + // GIVEN + TableColumnDefinitions colDefs = new TableColumnDefinitions(); + + // WHEN + DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs); + + // THEN + Assert.assertTrue(colDefs.getColumnDefinitions().isEmpty()); + } + + @Test + public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_not_change_width_not_all_definitions_are_auto() throws MeasurementException { + // GIVEN + TableColumnDefinitions colDefs = new TableColumnDefinitions(); + colDefs.newColumnDef().setWidth("30%", DOTS_PER_INCH); + colDefs.newColumnDef().setWidthAuto(); + + // WHEN + DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs); + + // THEN + Assert.assertEquals("30%", colDefs.get(0).getWidth()); + Assert.assertEquals("auto", colDefs.get(1).getWidth()); + } + // ===== INTERNAL UTILITIES private XWPFDocument convert(String infile, String outfile) throws Exception { From 52dff209e7f5660e7b182b00263b8dff89b9b4a0 Mon Sep 17 00:00:00 2001 From: Oleksandr Oboznyi Date: Wed, 8 Nov 2023 15:41:32 +0200 Subject: [PATCH 5/6] Link list numId to style abstractNumId and restart list levelt --- .../xml2docx/generator/DocxGenerator.java | 56 ++++++++++++++++++- .../xml2docx/TestDocxGenerator.java | 56 +++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java index 9ed8079..240885e 100644 --- a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java +++ b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java @@ -76,6 +76,7 @@ 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; @@ -3044,7 +3045,7 @@ private void setNumbering(XWPFDocument doc) { decNumId.setVal(bgiNumId); ctNumPr.setNumId(decNumId); } - doc.getNumbering().addNum(bgiNumId); + linkListNumIdToStyleAbstractIdAndRestartListLevels(doc, paras, bgiNumId); i++; } @@ -3258,4 +3259,57 @@ public static void setColumnsWidthInPercentagesIfAllHaveAutoWidth(TableColumnDef } } + /** + *

+ * Link list numId to style abstractNumId and restart list items ordering from 1 + *

+ *

+ * Assume all paras have the same style + *

+ * + * @param doc XWPF document + * @param paras list items + * @param numId list numId + */ + public static void linkListNumIdToStyleAbstractIdAndRestartListLevels(XWPFDocument doc, List 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); + } + + /** + *

+ * Link numId to style abstractNumId + *

+ * + * @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); + } + + /** + *

+ * Add level override from 1 to numId + *

+ * + * @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); + } + } diff --git a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java index a93ab46..3f275ec 100644 --- a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java +++ b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java @@ -3,6 +3,7 @@ import java.io.File; import java.io.FileInputStream; import java.math.BigInteger; +import java.util.Collections; import java.util.Iterator; import java.util.List; @@ -21,6 +22,7 @@ import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFPicture; import org.apache.poi.xwpf.usermodel.XWPFRun; +import org.apache.poi.xwpf.usermodel.XWPFStyle; import org.apache.poi.xwpf.usermodel.XWPFTable; import org.apache.poi.xwpf.usermodel.XWPFTableCell; import org.apache.poi.xwpf.usermodel.XWPFTableRow; @@ -28,10 +30,13 @@ import org.apache.xmlbeans.XmlObject; import org.junit.Assert; import org.junit.Test; +import org.mockito.Answers; import org.mockito.Mockito; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocument1; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumLvl; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr; @@ -682,6 +687,57 @@ public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_not_chang Assert.assertEquals("auto", colDefs.get(1).getWidth()); } + @Test + public void testLinkListNumIdToStyleAbstractIdAndRestartListLevels__should_set_abstract_num_id_and_restart_list_ordering() { + // GIVEN + XWPFDocument doc = Mockito.mock(XWPFDocument.class, Answers.RETURNS_DEEP_STUBS); + XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class); + XWPFStyle style = Mockito.mock(XWPFStyle.class, Answers.RETURNS_DEEP_STUBS); + CTNumLvl numLvl = Mockito.mock(CTNumLvl.class); + CTDecimalNumber startOverride = Mockito.mock(CTDecimalNumber.class); + BigInteger numId = BigInteger.valueOf(27); + BigInteger styleNumId = BigInteger.valueOf(89); + BigInteger styleAbstractNumId = BigInteger.valueOf(12); + Mockito.when(paragraph.getStyle()).thenReturn("FancyStyle123"); + Mockito.when(doc.getStyles().getStyle("FancyStyle123")).thenReturn(style); + Mockito.when(doc.getNumbering().getNum(styleNumId).getCTNum().getAbstractNumId().getVal()).thenReturn(styleAbstractNumId); + Mockito.when(style.getCTStyle().getPPr().getNumPr().getNumId().getVal()).thenReturn(styleNumId); + Mockito.when(doc.getNumbering().getNum(numId).getCTNum().addNewLvlOverride()).thenReturn(numLvl); + Mockito.when(numLvl.addNewStartOverride()).thenReturn(startOverride); + List paras = Collections.singletonList(paragraph); + + // WHEN + DocxGenerator.linkListNumIdToStyleAbstractIdAndRestartListLevels(doc, paras, numId); + + // THEN + Mockito.verify(doc.getNumbering()).addNum(styleAbstractNumId, numId); + Mockito.verify(numLvl).setIlvl(BigInteger.ZERO); + Mockito.verify(startOverride).setVal(BigInteger.ONE); + } + + @Test + public void testLinkListNumIdToStyleAbstractIdAndRestartListLevels__should_not_set_abstract_num_id_but_restart_list_ordering_if_style_does_not_exist() { + // GIVEN + XWPFDocument doc = Mockito.mock(XWPFDocument.class, Answers.RETURNS_DEEP_STUBS); + XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class); + CTNumLvl numLvl = Mockito.mock(CTNumLvl.class); + CTDecimalNumber startOverride = Mockito.mock(CTDecimalNumber.class); + BigInteger numId = BigInteger.valueOf(27); + Mockito.when(paragraph.getStyle()).thenReturn("FancyStyle123"); + Mockito.when(doc.getStyles().getStyle("FancyStyle123")).thenReturn(null); + Mockito.when(doc.getNumbering().getNum(numId).getCTNum().addNewLvlOverride()).thenReturn(numLvl); + Mockito.when(numLvl.addNewStartOverride()).thenReturn(startOverride); + List paras = Collections.singletonList(paragraph); + + // WHEN + DocxGenerator.linkListNumIdToStyleAbstractIdAndRestartListLevels(doc, paras, numId); + + // THEN + Mockito.verify(doc.getNumbering()).addNum(numId); + Mockito.verify(numLvl).setIlvl(BigInteger.ZERO); + Mockito.verify(startOverride).setVal(BigInteger.ONE); + } + // ===== INTERNAL UTILITIES private XWPFDocument convert(String infile, String outfile) throws Exception { From 42993978aa4a9c6c8e2ce7bbd026d2b83cb70cc1 Mon Sep 17 00:00:00 2001 From: Oleksandr Oboznyi Date: Wed, 8 Nov 2023 16:51:27 +0200 Subject: [PATCH 6/6] Resize images to fit container width --- .../xml2docx/generator/DocxGenerator.java | 156 ++++++++++++++---- .../xml2docx/TestDocxGenerator.java | 130 +++++++++++---- 2 files changed, 222 insertions(+), 64 deletions(-) diff --git a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java index 240885e..65284a6 100644 --- a/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java +++ b/src/main/java/org/wordinator/xml2docx/generator/DocxGenerator.java @@ -11,7 +11,9 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.math.BigDecimal; import java.math.BigInteger; +import java.math.RoundingMode; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; @@ -66,6 +68,7 @@ import org.apache.xmlbeans.impl.xb.xmlschema.SpaceAttribute.Space; import org.openxmlformats.schemas.officeDocument.x2006.sharedTypes.STOnOff1; import org.openxmlformats.schemas.officeDocument.x2006.sharedTypes.STVerticalAlignRun; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTAbstractNum; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBody; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBookmark; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTBorder; @@ -80,6 +83,7 @@ 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.CTPPrGeneral; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageNumber; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageSz; @@ -119,6 +123,8 @@ */ public class DocxGenerator { private static String NS_MATHML = "http://www.w3.org/1998/Math/MathML"; + private static final int MAX_CONTENT_WIDTH_IN_PX = 465; + private static final int DEFAULT_CELL_MARGIN = 4; int imageCounter = 0; // Used to keep track of count of images created. @@ -494,7 +500,7 @@ private XWPFParagraph handleBody( handleSection(doc, cursor.getObject()); } else if ("table".equals(tagName)) { XWPFTable table = doc.createTable(); - makeTable(table, cursor.getObject()); + makeTable(table, cursor.getObject(), MAX_CONTENT_WIDTH_IN_PX); } else if ("object".equals(tagName)) { // FIXME: This is currently unimplemented. makeObject(doc, cursor); @@ -1187,7 +1193,7 @@ private void makeHeaderFooter(XWPFHeaderFooter headerFooter, XmlObject xml) thro makeParagraph(p, cursor); } else if ("table".equals(tagName)) { XWPFTable table = headerFooter.createTable(0, 0); - makeTable(table, cursor.getObject()); + makeTable(table, cursor.getObject(), MAX_CONTENT_WIDTH_IN_PX); } else { // There are other body-level things that could go in a footnote but // we aren't worrying about them for now. @@ -1222,7 +1228,15 @@ private HeaderFooterType getHeaderFooterType(XmlCursor cursor) { * @throws Exception */ private XWPFParagraph makeParagraph(XWPFParagraph p, XmlCursor cursor) throws DocxGenerationException { - return makeParagraph(p, cursor, null); + return makeParagraph(p, cursor, null, MAX_CONTENT_WIDTH_IN_PX); + } + + /** + * @see DocxGenerator#makeParagraph(XWPFParagraph, XmlCursor) + * @param maxWidth Maximum content width in px + */ + private XWPFParagraph makeParagraph(XWPFParagraph p, XmlCursor cursor, int maxWidth) throws DocxGenerationException { + return makeParagraph(p, cursor, null, maxWidth); } /** @@ -1230,13 +1244,15 @@ private XWPFParagraph makeParagraph(XWPFParagraph p, XmlCursor cursor) throws Do * @param para The Word paragraph to construct * @param cursor Cursor pointing at the

element the paragraph will reflect. * @param additionalProperties Additional properties to add to the paragraph, i.e., from sections + * @param maxWidth Maximum content width in px * @return Paragraph (should be same object as passed in). * @throws Exception */ private XWPFParagraph makeParagraph( XWPFParagraph para, XmlCursor cursor, - Map additionalProperties) + Map additionalProperties, + int maxWidth) throws DocxGenerationException { cursor.push(); @@ -1328,7 +1344,7 @@ private XWPFParagraph makeParagraph( } else if ("hyperlink".equals(tagName)) { makeHyperlink(para, cursor); } else if ("image".equals(tagName)) { - makeImage(para, cursor); + makeImage(para, cursor, maxWidth); } else if ("object".equals(tagName)) { makeObject(para, cursor); } else if ("page-number-ref".equals(tagName)) { @@ -1632,7 +1648,7 @@ private void makeFootnote(XWPFParagraph para, XmlObject xml) throws DocxGenerati makeParagraph(p, cursor); } else if ("table".equals(tagName)) { XWPFTable table = note.createTable(); - makeTable(table, cursor.getObject()); + makeTable(table, cursor.getObject(), MAX_CONTENT_WIDTH_IN_PX); } else { // There are other body-level things that could go in a footnote but // we aren't worrying about them for now. @@ -1902,8 +1918,9 @@ private XWPFHyperlinkRun makeHyperlinkRun( * Construct an image reference * @param doc * @param cursor + * @param maxWidth Maximum image width in px */ - private void makeImage(XWPFParagraph para, XmlCursor cursor) throws DocxGenerationException { + private void makeImage(XWPFParagraph para, XmlCursor cursor, int maxWidth) throws DocxGenerationException { cursor.push(); // FIXME: This is all a bit scripty because of the need to get both the image data and @@ -2064,6 +2081,13 @@ private void makeImage(XWPFParagraph para, XmlCursor cursor) throws DocxGenerati height = (int)Math.round(intrinsicHeight * factor); } + maxWidth = adjustMaxWidthIfIndentationExists(para, maxWidth); + if (width > maxWidth) { + double ratio = width / (double) height; + width = maxWidth; + height = (int) (maxWidth / ratio); + } + // At this point, the measurement is pixels. If the original specification // was also pixels, we need to convert to inches and then back to pixels // in order to apply the dots-per-inch value. @@ -2254,9 +2278,10 @@ private void makeObject(XWPFDocument doc, XmlCursor cursor) throws DocxGeneratio * Construct a table. * @param table Table object to construct * @param xml The <table> element + * @param maxWidth Maximum content width in px * @throws DocxGenerationException */ - private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationException { + private void makeTable(XWPFTable table, XmlObject xml, int maxWidth) throws DocxGenerationException { // If the column widths are absolute measurements they can be set on the grid, // but if they are proportional, then they have to be set on at least the first @@ -2360,6 +2385,7 @@ private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationExce } setDefaultTableWidthIfNeeded(table); + setColumnsWidthInPercentages(colDefs, maxWidth); addTableGridWithColumnsIfNeeded(table, colDefs); // populate the rows and cells. @@ -2372,7 +2398,7 @@ private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationExce RowSpanManager rowSpanManager = new RowSpanManager(); do { // Process the rows - XWPFTableRow row = makeTableRow(table, cursor.getObject(), colDefs, rowSpanManager, defaults); + XWPFTableRow row = makeTableRow(table, cursor.getObject(), colDefs, rowSpanManager, defaults, maxWidth); row.setRepeatHeader(true); } while(cursor.toNextSibling()); } @@ -2386,7 +2412,7 @@ private void makeTable(XWPFTable table, XmlObject xml) throws DocxGenerationExce RowSpanManager rowSpanManager = new RowSpanManager(); do { // Process the rows - XWPFTableRow row = makeTableRow(table, cursor.getObject(), colDefs, rowSpanManager, defaults); + XWPFTableRow row = makeTableRow(table, cursor.getObject(), colDefs, rowSpanManager, defaults, maxWidth); // Adjust row as needed. row.getCtRow(); // For setting low-level properties. } while(cursor.toNextSibling()); @@ -2749,6 +2775,7 @@ private STBorder.Enum stBorderType(XWPFBorderType borderType) { * @param colDefs Column definitions * @param rowSpanManager Manages setting vertical spanning across multiple rows. * @param defaults Defaults inherited from the table (or elsewhere) + * @param maxWidth Maximum content width in px * @return Constructed row object * @throws DocxGenerationException */ @@ -2757,7 +2784,8 @@ private XWPFTableRow makeTableRow( XmlObject xml, TableColumnDefinitions colDefs, RowSpanManager rowSpanManager, - Map defaults) + Map defaults, + int maxWidth) throws DocxGenerationException { XmlCursor cursor = xml.newCursor(); XWPFTableRow row = table.createRow(); @@ -2917,7 +2945,16 @@ private XWPFTableRow makeTableRow( while (hasMore) { if (cursor.getName().equals(DocxConstants.QNAME_P_ELEM)) { XWPFParagraph p = cell.addParagraph(); - makeParagraph(p, cursor); + int widthInPx = convertColumnWidthToPx(colDef, maxWidth); + if (colspan != null && !colspan.equals("1")) { + int colspanValue = Integer.parseInt(colspan); + for (int i = colDef.getColumnIndex() + 1; i < colDef.getColumnIndex() + colspanValue; i++) { + widthInPx += convertColumnWidthToPx(colDefs.get(i), maxWidth); + } + } + int cellLeftMargin = table.getCellMarginLeft() != 0 ? table.getCellMarginLeft() : DEFAULT_CELL_MARGIN; + int cellRightMargin = table.getCellMarginRight() != 0 ? table.getCellMarginRight() : DEFAULT_CELL_MARGIN; + makeParagraph(p, cursor, widthInPx - cellLeftMargin - cellRightMargin); if (null != align) { if ("JUSTIFY".equalsIgnoreCase(align)) { // Issue 18: "BOTH" is the better match to "JUSTIFY" @@ -2940,7 +2977,8 @@ private XWPFTableRow makeTableRow( tblPr.addNewTblW(); XWPFTable nestedTable = new XWPFTable(ctTbl, cell); - makeTable(nestedTable, cursor.getObject()); + int widthInPx = convertColumnWidthToPx(colDef, maxWidth); + makeTable(nestedTable, cursor.getObject(), widthInPx); // for some reason this inserts two tables, where the // first one is empty. we need to remove that one. @@ -3186,7 +3224,6 @@ else if ("jpeg".equals(imgExtension) || * @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(); @@ -3233,29 +3270,31 @@ public static void setDefaultTableWidthIfNeeded(XWPFTable table) { * * @param colDefs table column definitions */ - public static void setColumnsWidthInPercentagesIfAllHaveAutoWidth(TableColumnDefinitions colDefs) { + public static void setColumnsWidthInPercentages(TableColumnDefinitions colDefs, int maxWidth) { if (colDefs.getColumnDefinitions().isEmpty()) { return; } - boolean autoWidth = true; + int dotsPerInch = 72; + double percentagesLeftForAutoWidth = 100; + List columnsWithAutoWidth = new ArrayList<>(); for (TableColumnDefinition colDef : colDefs.getColumnDefinitions()) { String specifiedWidth = colDef.getSpecifiedWidth(); - if (!"auto".equalsIgnoreCase(specifiedWidth)) { - autoWidth = false; - break; + if ("auto".equalsIgnoreCase(specifiedWidth)) { + columnsWithAutoWidth.add(colDef); + } else if (!"auto".equals(specifiedWidth) && !specifiedWidth.endsWith("%")) { + int px = Integer.parseInt(specifiedWidth); + double percentages = BigDecimal.valueOf(100.0 * px / maxWidth).setScale(4, RoundingMode.FLOOR).doubleValue(); + percentagesLeftForAutoWidth -= percentages; + setColumnWidthInPercentages(colDef, percentages, dotsPerInch); + } else if (specifiedWidth.endsWith("%")) { + double percentages = Double.parseDouble(specifiedWidth.substring(0, specifiedWidth.length() - 1)); + percentagesLeftForAutoWidth -= percentages; } } - 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); - } - } + if (!columnsWithAutoWidth.isEmpty()) { + int columnsCount = columnsWithAutoWidth.size(); + double width = BigDecimal.valueOf(percentagesLeftForAutoWidth / columnsCount).setScale(4, RoundingMode.FLOOR).doubleValue(); + columnsWithAutoWidth.forEach(it -> setColumnWidthInPercentages(it, width, dotsPerInch)); } } @@ -3312,4 +3351,61 @@ private static void addLevelOverrideFromOneToNumId(XWPFDocument doc, BigInteger startOverride.setVal(BigInteger.ONE); } + /** + *

+ * Convert column width to px + *

+ * + * @param columnDefinition table column definition + * @param maxWidth Maximum table width in px + * @return column width in px + */ + public static int convertColumnWidthToPx(TableColumnDefinition columnDefinition, int maxWidth) { + String width = columnDefinition.getWidth(); + if (width.endsWith("%")) { + return (int) (Double.parseDouble(width.substring(0, width.length() - 1)) * maxWidth / 100.0); + } + return Integer.parseInt(width); + } + + /** + *

+ * Adjusts width if paragraph has style with indentation + *

+ * + * @param para XWPF paragraph + * @param maxWidth Maximum width in px + * @return adjusted width + */ + public static int adjustMaxWidthIfIndentationExists(XWPFParagraph para, int maxWidth) { + String style = para.getStyle(); + XWPFStyle xwpfStyle = para.getDocument().getStyles().getStyle(style); + Object leftIndentation = null; + if (xwpfStyle != null) { + CTPPrGeneral pPr = xwpfStyle.getCTStyle().getPPr(); + if (pPr.getNumPr() != null) { + BigInteger numId = pPr.getNumPr().getNumId().getVal(); + XWPFNumbering numbering = para.getDocument().getNumbering(); + BigInteger abstractNumId = numbering.getNum(numId).getCTNum().getAbstractNumId().getVal(); + CTAbstractNum abstractNum = numbering.getAbstractNum(abstractNumId).getCTAbstractNum(); + leftIndentation = abstractNum.getLvlArray(0).getPPr().getInd().getLeft(); + } else if (pPr.getInd() != null) { + leftIndentation = pPr.getInd().getLeft(); + } + } + if (leftIndentation instanceof BigInteger) { + int scale = 20; + maxWidth -= ((BigInteger) leftIndentation).intValue() / scale; + } + return maxWidth; + } + + private static void setColumnWidthInPercentages(TableColumnDefinition colDef, double width, int dotsPerInch) { + try { + colDef.setWidth(width + "%", dotsPerInch); + } catch (MeasurementException e) { + throw new RuntimeException("Error setting column width: " + width, e); + } + } + } diff --git a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java index 3f275ec..b4e1d44 100644 --- a/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java +++ b/src/test/java/org/wordinator/xml2docx/TestDocxGenerator.java @@ -36,8 +36,11 @@ import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDecimalNumber; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTDocument1; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTFldChar; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTInd; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumLvl; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTNumPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPageMar; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPrGeneral; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTR; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTSectPr; import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTbl; @@ -48,6 +51,7 @@ import org.wordinator.xml2docx.generator.DocxConstants; import org.wordinator.xml2docx.generator.DocxGenerator; import org.wordinator.xml2docx.generator.MeasurementException; +import org.wordinator.xml2docx.generator.TableColumnDefinition; import org.wordinator.xml2docx.generator.TableColumnDefinitions; import junit.framework.TestCase; @@ -577,30 +581,6 @@ public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_mixed_ Mockito.verify(col2).setW(BigInteger.ZERO); } - @Test - public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_auto_width() { - // GIVEN - XWPFTable table = Mockito.mock(XWPFTable.class); - CTTbl ctTbl = Mockito.mock(CTTbl.class); - CTTblGrid ctTblGrid = Mockito.mock(CTTblGrid.class); - CTTblGridCol col1 = Mockito.mock(CTTblGridCol.class); - CTTblGridCol col2 = Mockito.mock(CTTblGridCol.class); - Mockito.when(table.getCTTbl()).thenReturn(ctTbl); - Mockito.when(ctTbl.addNewTblGrid()).thenReturn(ctTblGrid); - Mockito.when(ctTblGrid.addNewGridCol()).thenReturn(col1, col2); - - TableColumnDefinitions colDefs = new TableColumnDefinitions(); - colDefs.newColumnDef().setWidthAuto(); - colDefs.newColumnDef().setWidthAuto(); - - // WHEN - DocxGenerator.addTableGridWithColumnsIfNeeded(table, colDefs); - - // THEN - Mockito.verify(col1).setW(BigInteger.valueOf(2500)); - Mockito.verify(col2).setW(BigInteger.valueOf(2500)); - } - @Test public void testAddTableGridWithColumnsIfNeeded__should_create_table_grid_width_in_ints() throws MeasurementException { // GIVEN @@ -644,7 +624,7 @@ public void testSetDefaultTableWidthIfNeeded__should_set_100_percentages_width_a } @Test - public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_set_33_percentages_width() { + public void testSetColumnsWidthInPercentages__should_set_33_percentages_width() { // GIVEN TableColumnDefinitions colDefs = new TableColumnDefinitions(); colDefs.newColumnDef().setWidthAuto(); @@ -652,39 +632,39 @@ public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_set_33_pe colDefs.newColumnDef().setWidthAuto(); // WHEN - DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs); + DocxGenerator.setColumnsWidthInPercentages(colDefs, 100); // THEN - Assert.assertEquals("33%", colDefs.get(0).getWidth()); - Assert.assertEquals("33%", colDefs.get(1).getWidth()); - Assert.assertEquals("33%", colDefs.get(2).getWidth()); + Assert.assertEquals("33.3333%", colDefs.get(0).getWidth()); + Assert.assertEquals("33.3333%", colDefs.get(1).getWidth()); + Assert.assertEquals("33.3333%", colDefs.get(2).getWidth()); } @Test - public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_not_change_width_empty_definitions() { + public void testSetColumnsWidthInPercentages__should_not_change_width_empty_definitions() { // GIVEN TableColumnDefinitions colDefs = new TableColumnDefinitions(); // WHEN - DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs); + DocxGenerator.setColumnsWidthInPercentages(colDefs, 0); // THEN Assert.assertTrue(colDefs.getColumnDefinitions().isEmpty()); } @Test - public void testSetColumnsWidthInPercentagesIfAllHaveAutoWidth__should_not_change_width_not_all_definitions_are_auto() throws MeasurementException { + public void testSetColumnsWidthInPercentages__should_change_auto_width_by_percentages() throws MeasurementException { // GIVEN TableColumnDefinitions colDefs = new TableColumnDefinitions(); colDefs.newColumnDef().setWidth("30%", DOTS_PER_INCH); colDefs.newColumnDef().setWidthAuto(); // WHEN - DocxGenerator.setColumnsWidthInPercentagesIfAllHaveAutoWidth(colDefs); + DocxGenerator.setColumnsWidthInPercentages(colDefs, 100); // THEN Assert.assertEquals("30%", colDefs.get(0).getWidth()); - Assert.assertEquals("auto", colDefs.get(1).getWidth()); + Assert.assertEquals("70.0%", colDefs.get(1).getWidth()); } @Test @@ -738,6 +718,88 @@ public void testLinkListNumIdToStyleAbstractIdAndRestartListLevels__should_not_s Mockito.verify(startOverride).setVal(BigInteger.ONE); } + @Test + public void testConvertColumnWidthToPx__should_convert_if_percentages() throws MeasurementException { + // GIVEN + TableColumnDefinition columnDefinition = new TableColumnDefinition(Mockito.mock(TableColumnDefinitions.class)); + columnDefinition.setWidth("31.7498%", DOTS_PER_INCH); + + // WHEN + int result = DocxGenerator.convertColumnWidthToPx(columnDefinition, 123); + + // THEN + Assert.assertEquals(39, result); + } + + @Test + public void testConvertColumnWidthToPx__should_return_as_is_if_not_percentages() throws MeasurementException { + // GIVEN + TableColumnDefinition columnDefinition = new TableColumnDefinition(Mockito.mock(TableColumnDefinitions.class)); + columnDefinition.setWidth("31", DOTS_PER_INCH); + + // WHEN + int result = DocxGenerator.convertColumnWidthToPx(columnDefinition, 123); + + // THEN + Assert.assertEquals(31, result); + } + + @Test + public void testAdjustMaxWidthIfIndentationExists__should_adjust_width_if_style_with_num_section_exists() { + // GIVEN + XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class, Answers.RETURNS_DEEP_STUBS); + XWPFStyle style = Mockito.mock(XWPFStyle.class, Answers.RETURNS_DEEP_STUBS); + CTPPrGeneral pPr = Mockito.mock(CTPPrGeneral.class); + CTNumPr numPr = Mockito.mock(CTNumPr.class); + CTDecimalNumber numId = Mockito.mock(CTDecimalNumber.class); + Mockito.when(style.getCTStyle().getPPr()).thenReturn(pPr); + Mockito.when(pPr.getNumPr()).thenReturn(numPr); + Mockito.when(numPr.getNumId()).thenReturn(numId); + Mockito.when(numId.getVal()).thenReturn(BigInteger.valueOf(11)); + Mockito.when(paragraph.getStyle()).thenReturn("SomeStyle123"); + Mockito.when(paragraph.getDocument().getStyles().getStyle("SomeStyle123")).thenReturn(style); + Mockito.when(paragraph.getDocument().getNumbering().getNum(BigInteger.valueOf(11)).getCTNum().getAbstractNumId().getVal()).thenReturn(BigInteger.valueOf(82)); + Mockito.when(paragraph.getDocument().getNumbering().getAbstractNum(BigInteger.valueOf(82)).getCTAbstractNum().getLvlArray(0).getPPr().getInd().getLeft()).thenReturn(BigInteger.valueOf(360)); + + // WHEN + int result = DocxGenerator.adjustMaxWidthIfIndentationExists(paragraph, 500); + + // THEN + Assert.assertEquals(482, result); + } + + @Test + public void testAdjustMaxWidthIfIndentationExists__should_adjust_width_if_style_with_ind_section_exists() { + // GIVEN + XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class, Answers.RETURNS_DEEP_STUBS); + XWPFStyle style = Mockito.mock(XWPFStyle.class, Answers.RETURNS_DEEP_STUBS); + CTPPrGeneral pPr = Mockito.mock(CTPPrGeneral.class); + CTInd ind = Mockito.mock(CTInd.class); + Mockito.when(style.getCTStyle().getPPr()).thenReturn(pPr); + Mockito.when(paragraph.getStyle()).thenReturn("SomeStyle123"); + Mockito.when(paragraph.getDocument().getStyles().getStyle("SomeStyle123")).thenReturn(style); + Mockito.when(pPr.getInd()).thenReturn(ind); + Mockito.when(ind.getLeft()).thenReturn(BigInteger.valueOf(720)); + + // WHEN + int result = DocxGenerator.adjustMaxWidthIfIndentationExists(paragraph, 500); + + // THEN + Assert.assertEquals(464, result); + } + + @Test + public void testAdjustMaxWidthIfIndentationExists__should_not_adjust_width_if_no_style() { + // GIVEN + XWPFParagraph paragraph = Mockito.mock(XWPFParagraph.class, Answers.RETURNS_DEEP_STUBS); + + // WHEN + int result = DocxGenerator.adjustMaxWidthIfIndentationExists(paragraph, 500); + + // THEN + Assert.assertEquals(500, result); + } + // ===== INTERNAL UTILITIES private XWPFDocument convert(String infile, String outfile) throws Exception {