diff --git a/build.gradle b/build.gradle index cc34524..2bd3e5f 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ plugins { } allprojects { + apply plugin: 'maven' apply plugin: 'groovy' apply plugin: 'idea' apply plugin: 'eclipse' @@ -18,7 +19,7 @@ allprojects { apply plugin: 'nebula.provided-base' group = 'com.craigburke.document' - version = '0.4.15' + version = '0.4.16-SNAPSHOT' targetCompatibility = 1.6 repositories { diff --git a/core/src/main/groovy/com/craigburke/document/core/Dimension.groovy b/core/src/main/groovy/com/craigburke/document/core/Dimension.groovy new file mode 100644 index 0000000..22f8bba --- /dev/null +++ b/core/src/main/groovy/com/craigburke/document/core/Dimension.groovy @@ -0,0 +1,12 @@ +package com.craigburke.document.core + +import groovy.transform.Immutable + +/** + * Two dimensional width/height. + */ +@Immutable +class Dimension { + BigDecimal width + BigDecimal height +} diff --git a/core/src/main/groovy/com/craigburke/document/core/Document.groovy b/core/src/main/groovy/com/craigburke/document/core/Document.groovy index 5322dff..bd535cc 100644 --- a/core/src/main/groovy/com/craigburke/document/core/Document.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/Document.groovy @@ -9,9 +9,13 @@ import static com.craigburke.document.core.UnitUtil.inchToPoint class Document extends BlockNode { static Margin defaultMargin = new Margin(top: 72, bottom: 72, left: 72, right: 72) + private static final String PORTRAIT = 'portrait' + private static final String LANDSCAPE = 'landscape' + int pageCount - final int width = inchToPoint(8.5) - final int height = inchToPoint(11) + int width = inchToPoint(PaperSize.LETTER.width) + int height = inchToPoint(PaperSize.LETTER.height) + String orientation = PORTRAIT def template def header @@ -41,4 +45,54 @@ class Document extends BlockNode { List children = [] List embeddedFonts = [] + /** + * Set width and height of the document. + * + * @param arg name of a standard paper size ("a4", "letter", "legal") + */ + void setSize(String arg) { + setSize(PaperSize.get(arg)) + } + + /** + * Set width and height of the document. + * + * @param arg a Dimension instance + */ + void setSize(Dimension arg) { + width = inchToPoint(arg.width) + height = inchToPoint(arg.height) + } + + /** + * Set width and height of the document. + * + * @param args width, height + */ + void setSize(List args) { + width = args[0] + height = args[1] + } + + /** + * Set document orientation. + * + * @param arg "portrait" or "landscape" + */ + void setOrientation(String arg) { + arg = arg.toLowerCase() + if (arg != PORTRAIT && arg != LANDSCAPE) { + throw new IllegalArgumentException("invalid orientation: $arg, only '$PORTRAIT' or '$LANDSCAPE' allowed") + } + if (this.@orientation != arg) { + this.@orientation = arg + def tmp = width + width = height + height = tmp + } + } + + boolean isLandscape() { + this.orientation == LANDSCAPE + } } diff --git a/core/src/main/groovy/com/craigburke/document/core/Image.groovy b/core/src/main/groovy/com/craigburke/document/core/Image.groovy index 3972c11..8440f09 100644 --- a/core/src/main/groovy/com/craigburke/document/core/Image.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/Image.groovy @@ -1,5 +1,7 @@ package com.craigburke.document.core +import java.security.MessageDigest + /** * Image node * @author Craig Burke @@ -9,9 +11,29 @@ class Image extends BaseNode { ImageType type = ImageType.JPG Integer width Integer height + String url byte[] data - - void setType(String value) { - type = Enum.valueOf(ImageType, value.toUpperCase()) + + void setType(String value) { + type = Enum.valueOf(ImageType, value.toUpperCase()) + } + + byte[] getData() { + if(this.@data == null && url != null) { + this.data = new URL(url).bytes + } + this.@data + } + + def withInputStream(Closure work) { + work.call(new ByteArrayInputStream(getData())) + } + + String getHash() { + Formatter hexHash = new Formatter() + MessageDigest.getInstance('SHA-1').digest(getData()).each { + b -> hexHash.format('%02x', b) + } + hexHash.toString() } } diff --git a/core/src/main/groovy/com/craigburke/document/core/PaperSize.groovy b/core/src/main/groovy/com/craigburke/document/core/PaperSize.groovy new file mode 100644 index 0000000..7e480f2 --- /dev/null +++ b/core/src/main/groovy/com/craigburke/document/core/PaperSize.groovy @@ -0,0 +1,39 @@ +package com.craigburke.document.core + +/** + * Standard paper size utility. Returned dimensions are inches. + */ +class PaperSize { + + static final Dimension A1 = new Dimension(23.4, 33.1) + static final Dimension A2 = new Dimension(16.5, 23.4) + static final Dimension A3 = new Dimension(11.7, 16.5) + static final Dimension A4 = new Dimension(8.27, 11.7) + static final Dimension A5 = new Dimension(5.83, 8.27) + static final Dimension A6 = new Dimension(4.13, 5.83) + static final Dimension LETTER = new Dimension(8.5, 11) + static final Dimension LEGAL = new Dimension(8.5, 14) + + static Dimension get(String name) { + switch (name.toLowerCase()) { + case 'a1': + return A1 + case 'a2': + return A2 + case 'a3': + return A3 + case 'a4': + return A4 + case 'a5': + return A5 + case 'a6': + return A6 + case 'letter': + return LETTER + case 'legal': + return LEGAL + default: + throw new IllegalArgumentException("invalid paper size: $name") + } + } +} diff --git a/core/src/main/groovy/com/craigburke/document/core/Toc.groovy b/core/src/main/groovy/com/craigburke/document/core/Toc.groovy new file mode 100644 index 0000000..397ef83 --- /dev/null +++ b/core/src/main/groovy/com/craigburke/document/core/Toc.groovy @@ -0,0 +1,10 @@ +package com.craigburke.document.core + +/** + * Table of Contents. + */ +class Toc extends BaseNode { + Toc() {} + + Toc(Map attributes) {} +} diff --git a/core/src/main/groovy/com/craigburke/document/core/UnitCategory.groovy b/core/src/main/groovy/com/craigburke/document/core/UnitCategory.groovy index f315b6a..d885454 100644 --- a/core/src/main/groovy/com/craigburke/document/core/UnitCategory.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/UnitCategory.groovy @@ -10,6 +10,7 @@ class UnitCategory { BigDecimal getInch() { this * UnitUtil.POINTS_PER_INCH } BigDecimal getCentimeters() { this * UnitUtil.POINTS_PER_CENTIMETER } BigDecimal getCentimeter() { this * UnitUtil.POINTS_PER_CENTIMETER } + BigDecimal getCm() { this * UnitUtil.POINTS_PER_CENTIMETER } BigDecimal getPt() { this } BigDecimal getPx() { this } } diff --git a/core/src/main/groovy/com/craigburke/document/core/UnitUtil.groovy b/core/src/main/groovy/com/craigburke/document/core/UnitUtil.groovy index 7a3293a..3fa9eab 100644 --- a/core/src/main/groovy/com/craigburke/document/core/UnitUtil.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/UnitUtil.groovy @@ -6,7 +6,8 @@ package com.craigburke.document.core */ class UnitUtil { static final BigDecimal POINTS_PER_INCH = 72 - static final BigDecimal POINTS_PER_CENTIMETER = 28.346457 + static final BigDecimal CENTIMETER_PER_INCH = 2.54 + static final BigDecimal POINTS_PER_CENTIMETER = POINTS_PER_INCH / CENTIMETER_PER_INCH static final BigDecimal PICA_POINTS = 6 static final BigDecimal TWIP_POINTS = 20 static final BigDecimal EIGTH_POINTS = 8 @@ -21,6 +22,14 @@ class UnitUtil { point / POINTS_PER_INCH } + static BigDecimal cmToPoint(BigDecimal cm) { + cm * POINTS_PER_CENTIMETER + } + + static BigDecimal pointToCm(BigDecimal point) { + point / POINTS_PER_CENTIMETER + } + static BigDecimal pointToPica(BigDecimal point) { point * PICA_POINTS } @@ -61,4 +70,11 @@ class UnitUtil { emu / EMU_POINTS } + static BigDecimal inchToCm(BigDecimal inch) { + inch * CENTIMETER_PER_INCH + } + + static BigDecimal cmToInch(BigDecimal inch) { + inch / CENTIMETER_PER_INCH + } } diff --git a/core/src/main/groovy/com/craigburke/document/core/builder/DocumentBuilder.groovy b/core/src/main/groovy/com/craigburke/document/core/builder/DocumentBuilder.groovy index dab958a..a89fe82 100644 --- a/core/src/main/groovy/com/craigburke/document/core/builder/DocumentBuilder.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/builder/DocumentBuilder.groovy @@ -26,6 +26,7 @@ import com.craigburke.document.core.factory.CellFactory import com.craigburke.document.core.Document import com.craigburke.document.core.Font +import com.craigburke.document.core.factory.TocFactory /** * Document Builder base class @@ -184,6 +185,7 @@ abstract class DocumentBuilder extends FactoryBuilderSupport { registerFactory('heading4', new HeadingFactory()) registerFactory('heading5', new HeadingFactory()) registerFactory('heading6', new HeadingFactory()) + registerFactory('toc', new TocFactory()) } } diff --git a/core/src/main/groovy/com/craigburke/document/core/factory/ImageFactory.groovy b/core/src/main/groovy/com/craigburke/document/core/factory/ImageFactory.groovy index 9769d49..165f3ab 100644 --- a/core/src/main/groovy/com/craigburke/document/core/factory/ImageFactory.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/factory/ImageFactory.groovy @@ -6,7 +6,6 @@ import com.craigburke.document.core.TextBlock import javax.imageio.ImageIO import java.awt.image.BufferedImage -import java.security.MessageDigest /** * Factory for image nodes @@ -22,10 +21,18 @@ class ImageFactory extends AbstractFactory { Image image = new Image(attributes) if (!image.width || !image.height) { - InputStream inputStream = new ByteArrayInputStream(image.data) - BufferedImage bufferedImage = ImageIO.read(inputStream) - image.width = bufferedImage.width - image.height = bufferedImage.height + BufferedImage bufferedImage = image.withInputStream { ImageIO.read(it) } + if(bufferedImage == null) { + throw new IllegalStateException("could not read image $attributes") + } + if(image.width) { + image.height = image.width * (bufferedImage.height / bufferedImage.width) + } else if(image.height) { + image.width = image.height * (bufferedImage.width / bufferedImage.height) + } else { + image.width = bufferedImage.width + image.height = bufferedImage.height + } } if (!image.name || builder.imageFileNames.contains(image.name)) { @@ -46,11 +53,7 @@ class ImageFactory extends AbstractFactory { } String generateImageName(Image image) { - Formatter hexHash = new Formatter() - MessageDigest.getInstance('SHA-1').digest(image.data).each { - b -> hexHash.format('%02x', b) - } - "${hexHash}.${image.type == ImageType.JPG ? 'jpg' : 'png'}" + "${image.hash}.${image.type == ImageType.JPG ? 'jpg' : 'png'}" } } diff --git a/core/src/main/groovy/com/craigburke/document/core/factory/TocFactory.groovy b/core/src/main/groovy/com/craigburke/document/core/factory/TocFactory.groovy new file mode 100644 index 0000000..c672739 --- /dev/null +++ b/core/src/main/groovy/com/craigburke/document/core/factory/TocFactory.groovy @@ -0,0 +1,18 @@ +package com.craigburke.document.core.factory + +import com.craigburke.document.core.Toc + +/** + * Table of Contents factory. + */ +class TocFactory extends AbstractFactory { + + boolean isLeaf() { true } + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + Toc toc = new Toc(attributes) + toc.parent = builder.parentName == 'create' ? builder.document : builder.current + builder.setNodeProperties(toc, attributes, 'toc') + toc + } +} diff --git a/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy b/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy index 3b42721..aff2828 100644 --- a/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy +++ b/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy @@ -50,6 +50,66 @@ class BuilderSpec extends Specification { testFile?.delete() } + def "create default letter size document"() { + when: + def result = builder.create { + document(margin: [top: 2.cm, bottom: 1.cm]) { + paragraph(align: 'center', font: [size: 24.pt]) { + text 'ISO 216' + } + } + } + + then: + result.document.width == 612 // 8.5 inch * 72 DPI + result.document.height == 792 // 11 inch * 72 DPI + } + + def "create A4 document"() { + when: + def result = builder.create { + document(size: 'A4', margin: [top: 2.cm, bottom: 1.cm]) { + paragraph(align: 'center', font: [size: 24.pt]) { + text 'ISO 216' + } + } + } + + then: + result.document.width == 595 // 8.27 inch * 72 DPI + result.document.height == 842 // 11.7 inch * 72 DPI + } + + def "create document with custom size"() { + when: + def result = builder.create { + document(size: [14.8.cm, 21.cm], margin: [top: 2.cm, bottom: 1.cm]) { + paragraph(align: 'center', font: [size: 24.pt]) { + text 'ISO 216' + } + } + } + + then: + result.document.width == 419 // 8.27 inch * 72 DPI + result.document.height == 595 // 11.7 inch * 72 DPI + } + + def "use landscape orientation"() { + when: + def result = builder.create { + document(size: 'A4', orientation: 'landscape', margin: [top: 2.cm, bottom: 1.cm]) { + paragraph(align: 'center', font: [size: 24.pt]) { + text 'Landscape' + } + } + } + + then: + result.document.width == 842 // 11.7 inch * 72 DPI + result.document.height == 595 // 8.27 inch * 72 DPI + } + def "use typographic units"() { when: builder.create { @@ -171,6 +231,58 @@ class BuilderSpec extends Specification { thrown(Exception) } + def "Image can be loaded from URL"() { + when: + def result = builder.create { + document { + paragraph { + image(url: "http://dummyimage.com/600x400") + } + } + } + + then: + TextBlock paragraph = result.document.children[0] + Image image = paragraph.children[0] + image.data != null + image.width == 600 + image.height == 400 + } + + def "Image should have correct aspect ratio if only width is specified"() { + when: + def result = builder.create { + document { + paragraph { + image(data: imageData, width: 250.px) // cheeseburger.jpg is 500x431 + } + } + } + + then: + TextBlock paragraph = result.document.children[0] + Image image = paragraph.children[0] + image.width == 250 + image.height == 215 + } + + def "Image should have correct aspect ratio if only height is specified"() { + when: + def result = builder.create { + document { + paragraph { + image(data: imageData, height: 216.px) // cheeseburger.jpg is 500x431 + } + } + } + + then: + TextBlock paragraph = result.document.children[0] + Image image = paragraph.children[0] + image.width == 250 + image.height == 216 + } + def "create a simple paragraph"() { when: def result = builder.create { diff --git a/pdf/build.gradle b/pdf/build.gradle index 66ea678..c880aa0 100644 --- a/pdf/build.gradle +++ b/pdf/build.gradle @@ -1,6 +1,6 @@ dependencies { compile project(':core') - compile 'org.apache.pdfbox:pdfbox:1.8.8' + compile 'org.apache.pdfbox:pdfbox:1.8.11' } project.ext { @@ -10,4 +10,4 @@ project.ext { name: 'Mozilla Public License, Version 2.0', url : 'https://www.mozilla.org/MPL/2.0/' ] -} \ No newline at end of file +} diff --git a/pdf/src/main/groovy/com/craigburke/document/builder/PdfDocument.groovy b/pdf/src/main/groovy/com/craigburke/document/builder/PdfDocument.groovy index c7ef039..104d38d 100644 --- a/pdf/src/main/groovy/com/craigburke/document/builder/PdfDocument.groovy +++ b/pdf/src/main/groovy/com/craigburke/document/builder/PdfDocument.groovy @@ -3,6 +3,7 @@ package com.craigburke.document.builder import com.craigburke.document.core.Document import org.apache.pdfbox.pdmodel.PDDocument import org.apache.pdfbox.pdmodel.PDPage +import org.apache.pdfbox.pdmodel.common.PDRectangle import org.apache.pdfbox.pdmodel.edit.PDPageContentStream /** @@ -36,8 +37,16 @@ class PdfDocument { currentPage.mediaBox.height - document.margin.bottom } + private PDRectangle getRectangle(BigDecimal width, BigDecimal height) { + new PDRectangle(width.floatValue(), height.floatValue()) + } + void addPage() { def newPage = new PDPage() + newPage.setMediaBox(getRectangle(document.width, document.height)) + if(document.isLandscape()) { + newPage.setRotation(90) + } pages << newPage pageNumber++ diff --git a/word/build.gradle b/word/build.gradle index 17e0b93..13b2427 100644 --- a/word/build.gradle +++ b/word/build.gradle @@ -1,7 +1,7 @@ dependencies { compile project(':core') - String POI_VERSION = '3.10.1' + String POI_VERSION = '3.13' testCompile 'xml-apis:xml-apis:1.4.01' testCompile "org.apache.poi:poi:${POI_VERSION}" @@ -16,4 +16,4 @@ project.ext { name: 'Mozilla Public License, Version 2.0', url : 'https://www.mozilla.org/MPL/2.0/' ] -} \ No newline at end of file +} diff --git a/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy b/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy index 8640ce8..f2c18ee 100644 --- a/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy +++ b/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy @@ -18,6 +18,7 @@ import com.craigburke.document.core.PageBreak import com.craigburke.document.core.TextBlock import com.craigburke.document.core.Table import com.craigburke.document.core.Text +import com.craigburke.document.core.Toc import groovy.transform.InheritConstructors import com.craigburke.document.core.builder.DocumentBuilder @@ -33,6 +34,8 @@ class WordDocumentBuilder extends DocumentBuilder { private static final String PAGE_NUMBER_PLACEHOLDER = '##pageNumber##' private static final Map RUN_TEXT_OPTIONS = ['xml:space': 'preserve'] + int bookmark = 1 + void initializeDocument(Document document, OutputStream out) { document.element = new WordDocument(out) } @@ -48,6 +51,7 @@ class WordDocumentBuilder extends DocumentBuilder { dateGenerated: new Date() ) + renderStyles([:]) def header = renderHeader(headerFooterOptions) def footer = renderFooter(headerFooterOptions) @@ -62,12 +66,14 @@ class WordDocumentBuilder extends DocumentBuilder { addPageBreak(builder) } else if (child instanceof Table) { addTable(builder, child) + } else if (child instanceof Toc) { + addTableOfContents(builder) } } w.sectPr { w.pgSz('w:h': pointToTwip(document.height), 'w:w': pointToTwip(document.width), - 'w:orient': 'portrait' + 'w:orient': document.orientation ) w.pgMar('w:bottom': pointToTwip(document.margin.bottom), 'w:top': pointToTwip(document.margin.top), @@ -128,6 +134,98 @@ class WordDocumentBuilder extends DocumentBuilder { } + void renderStyles(Map options) { + wordDocument.generateDocumentPart(BasicDocumentPartTypes.STYLES) { builder -> + w.styles { + w.latentStyles('w:defLockedState': "0", 'w:defUIPriority': "99", 'w:defSemiHidden': "1", + 'w:defUnhideWhenUsed': "1", 'w:defQFormat': "0", 'w:count': "276") { + w.lsdException('w:name': "Normal", 'w:semiHidden': "0", 'w:uiPriority': "0", 'w:unhideWhenUsed': "0", 'w:qFormat': "1") + w.lsdException('w:name': "heading 1", 'w:semiHidden': "0", 'w:uiPriority': "9", 'w:unhideWhenUsed': "0", 'w:qFormat': "1") + for (n in 2..9) { + w.lsdException('w:name': "heading $n", 'w:uiPriority': "9", 'w:qFormat': "1") + } + for (n in 1..9) { + w.lsdException('w:name': "toc $n", 'w:uiPriority': "39") + } + w.lsdException('w:name': "caption", 'w:uiPriority': "35", 'w:qFormat': "1") + w.lsdException('w:name': "Title", 'w:semiHidden': "0", 'w:uiPriority': "10", 'w:unhideWhenUsed': "0", 'w:qFormat': "1") + + w.lsdException('w:name': "TOC Heading", 'w:uiPriority': "39", 'w:qFormat': "1") + } + w.style('w:type': "paragraph", 'w:default': "1", 'w:styleId': "Normal") { + w.name('w:val': "Normal") + w.qFormat() + } + w.style('w:type': "paragraph", 'w:styleId': "Heading1") { + w.name('w:val': "heading 1") + w.basedOn('w:val': "Normal") + w.next('w:val': "Normal") + //w.link('w:val': "Heading1Char") + w.uiPriority('w:val': "9") + w.qFormat() + //w.rsid('w:val': "00676A8F") + w.pPr { + w.keepNext() + w.keepLines() + w.spacing('w:before': "480") + w.outlineLvl('w:val': "0") + } + } + w.style('w:type': "paragraph", 'w:styleId': "Heading2") { + w.name('w:val': "heading 2") + w.basedOn('w:val': "Normal") + w.next('w:val': "Normal") + //w.link('w:val': "Heading2Char") + w.uiPriority('w:val': "9") + w.qFormat() + //w.rsid('w:val': "00676A8F") + w.pPr { + w.keepNext() + w.keepLines() + w.spacing('w:before': "480") + w.outlineLvl('w:val': "0") + } + } + } + } + } + + void addTableOfContents(builder) { + builder.w.p { + w.pPr { + w.pStyle('w:val': "TOC1") + w.tabs { + w.tab('w:val': "right", 'w:leader': "dot", 'w:pos': "9016") + } + w.rPr { + w.noProof() + } + } + w.r { + w.fldChar('w:fldCharType': 'begin') + } + w.r { + w.instrText('xml:space': 'preserve') { + mkp.yieldUnescaped('TOC \\o "1-3" \\h \\z \\u') + } + } + w.r { + w.fldChar('w:fldCharType': 'separate') + } + w.r { + w.rPr { + w.noProof() + } + w.t { + mkp.yieldUnescaped('RIGHT-CLICK TO UPDATE FIELD.') + } + } + w.r { + w.fldChar('w:fldCharType': 'end') + } + } + } + void addPageBreak(builder) { builder.w.p { w.r { @@ -199,27 +297,27 @@ class WordDocumentBuilder extends DocumentBuilder { if (paragraph instanceof Heading && stylesEnabled) { w.pStyle 'w:val': "Heading${paragraph.level}" - } - - String lineRule = (paragraph.lineSpacing) ? 'exact' : 'auto' - BigDecimal lineValue = (paragraph.lineSpacing) ? - pointToTwip(paragraph.lineSpacing) : (paragraph.lineSpacingMultiplier * 240) - w.spacing( - 'w:before': calculatedSpacingBefore(paragraph), - 'w:after': calculateSpacingAfter(paragraph), - 'w:lineRule': lineRule, - 'w:line': lineValue - ) - w.ind( - 'w:start': pointToTwip(paragraph.margin.left), - 'w:left': pointToTwip(paragraph.margin.left), - 'w:right': pointToTwip(paragraph.margin.right), - 'w:end': pointToTwip(paragraph.margin.right) - ) - w.jc('w:val': paragraph.align.value) - - if (paragraph instanceof Heading) { w.outlineLvl('w:val': "${paragraph.level - 1}") + if(! paragraph.ref) { + paragraph.ref = "_Toc${bookmark++}" + } + } else { + String lineRule = (paragraph.lineSpacing) ? 'exact' : 'auto' + BigDecimal lineValue = (paragraph.lineSpacing) ? + pointToTwip(paragraph.lineSpacing) : (paragraph.lineSpacingMultiplier * 240) + w.spacing( + 'w:before': calculatedSpacingBefore(paragraph), + 'w:after': calculateSpacingAfter(paragraph), + 'w:lineRule': lineRule, + 'w:line': lineValue + ) + w.ind( + 'w:start': pointToTwip(paragraph.margin.left), + 'w:left': pointToTwip(paragraph.margin.left), + 'w:right': pointToTwip(paragraph.margin.right), + 'w:end': pointToTwip(paragraph.margin.right) + ) + w.jc('w:val': paragraph.align.value) } } @@ -228,22 +326,29 @@ class WordDocumentBuilder extends DocumentBuilder { w.bookmarkStart('w:id': paragraphLinkId, 'w:name': paragraph.ref) } paragraph.children.each { child -> - switch (child.getClass()) { - case Text: - if (child.url?.startsWith('#') && child.url.size() > 1) { - addLink(builder, child) - } else if (child.ref) { - addBookmark(builder, child) - } else { - addTextRun(builder, child.font as Font, child.value as String) - } - break - case Image: - addImageRun(builder, child) - break - case LineBreak: - addLineBreakRun(builder) - break + if(paragraph instanceof Heading) { + w.r { + w.lastRenderedPageBreak() + w.t(child.value as String, RUN_TEXT_OPTIONS) + } + } else { + switch (child.getClass()) { + case Text: + if (child.url?.startsWith('#') && child.url.size() > 1) { + addLink(builder, child) + } else if (child.ref) { + addBookmark(builder, child) + } else { + addTextRun(builder, child.font as Font, child.value as String) + } + break + case Image: + addImageRun(builder, child) + break + case LineBreak: + addLineBreakRun(builder) + break + } } } if (paragraph.ref) { @@ -253,7 +358,7 @@ class WordDocumentBuilder extends DocumentBuilder { } protected boolean isStylesEnabled() { - false + true } void addBookmark(builder, Text text) {