All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.craigburke.document.builder.WordDocumentBuilder.groovy Maven / Gradle / Ivy

package com.craigburke.document.builder

import static com.craigburke.document.core.UnitUtil.pointToEigthPoint
import static com.craigburke.document.core.UnitUtil.pointToEmu
import static com.craigburke.document.core.UnitUtil.pointToTwip
import static com.craigburke.document.core.UnitUtil.pointToHalfPoint

import com.craigburke.document.core.HeaderFooterOptions
import com.craigburke.document.core.Heading
import com.craigburke.document.core.builder.RenderState
import com.craigburke.document.core.BlockNode
import com.craigburke.document.core.Cell
import com.craigburke.document.core.Row
import com.craigburke.document.core.Font
import com.craigburke.document.core.Image
import com.craigburke.document.core.LineBreak
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 groovy.transform.InheritConstructors

import com.craigburke.document.core.builder.DocumentBuilder
import com.craigburke.document.core.Document

/**
 * Builder for Word documents
 * @author Craig Burke
 */
@InheritConstructors
class WordDocumentBuilder extends DocumentBuilder {

    private static final String PAGE_NUMBER_PLACEHOLDER = '##pageNumber##'
    private static final Map RUN_TEXT_OPTIONS = ['xml:space': 'preserve']

    void initializeDocument(Document document, OutputStream out) {
        document.element = new WordDocument(out)
    }

    WordDocument getWordDocument() {
        document.element
    }

    void writeDocument(Document document, OutputStream out) {
        def headerFooterOptions = new HeaderFooterOptions(
                pageNumber: PAGE_NUMBER_PLACEHOLDER,
                pageCount: document.pageCount,
                dateGenerated: new Date()
        )

        def header = renderHeader(headerFooterOptions)
        def footer = renderFooter(headerFooterOptions)

        renderState = RenderState.PAGE
        wordDocument.generateDocument { builder ->
            w.document {
                w.body {
                    document.children.each { child ->
                        if (child instanceof TextBlock) {
                            addParagraph(builder, child)
                        } else if (child instanceof PageBreak) {
                            addPageBreak(builder)
                        } else if (child instanceof Table) {
                            addTable(builder, child)
                        }
                    }
                    w.sectPr {
                        w.pgSz('w:h': pointToTwip(document.height),
                                'w:w': pointToTwip(document.width),
                                'w:orient': 'portrait'
                        )
                        w.pgMar('w:bottom': pointToTwip(document.margin.bottom),
                                'w:top': pointToTwip(document.margin.top),
                                'w:right': pointToTwip(document.margin.right),
                                'w:left': pointToTwip(document.margin.left),
                                'w:footer': pointToTwip(footer ? footer.node.margin.bottom : 0),
                                'w:header': pointToTwip(header ? header.node.margin.top : 0)
                        )
                        if (header) {
                            w.headerReference('r:id': header.id, 'w:type': 'default')
                        }
                        if (footer) {
                            w.footerReference('r:id': footer.id, 'w:type': 'default')
                        }
                    }
                }
            }
        }

        renderState = RenderState.CUSTOM
        document.element.write()
    }

    def renderHeader(HeaderFooterOptions options) {
        def header = [:]
        if (document.header) {
            renderState = RenderState.HEADER
            header.node = document.header(options)
            header.id = wordDocument.generateDocumentPart(BasicDocumentPartTypes.HEADER) { builder ->
                w.hdr {
                    renderHeaderFooterNode(builder, header.node as BlockNode)
                }
            }
        }
        header
    }

    def renderFooter(HeaderFooterOptions options) {
        def footer = [:]
        if (document.footer) {
            renderState = RenderState.FOOTER
            footer.node = document.footer(options)
            footer.id = wordDocument.generateDocumentPart(BasicDocumentPartTypes.FOOTER) { builder ->
                w.hdr {
                    renderHeaderFooterNode(builder, footer.node as BlockNode)
                }
            }
        }
        footer
    }

    void renderHeaderFooterNode(builder, BlockNode node) {
        if (node instanceof TextBlock) {
            addParagraph(builder, node)
        } else {
            addTable(builder, node)
        }

    }

    void addPageBreak(builder) {
        builder.w.p {
            w.r {
                w.br('w:type': 'page')
            }
        }
    }

    int calculateSpacingAfter(BlockNode node) {
        int totalSpacing

        switch (renderState) {
            case RenderState.PAGE:
                totalSpacing = node.margin.bottom

                def items = node.parent.children
                int index = items.findIndexOf { it == node }

                if (index != items.size() - 1) {
                    def nextSibling = items[index + 1]
                    if (nextSibling instanceof BlockNode) {
                        totalSpacing += nextSibling.margin.top
                    }
                }
                break

            case RenderState.HEADER:
                totalSpacing = node.margin.bottom
                break

            case RenderState.FOOTER:
                totalSpacing = 0
        }
        pointToTwip(totalSpacing)
    }

    int calculatedSpacingBefore(BlockNode node) {
        int totalSpacing

        switch (renderState) {
            case RenderState.PAGE:
                totalSpacing = node.margin.top
                def items = node.parent.children
                int index = items.findIndexOf { it == node }
                if (index > 0) {
                    def previousSibling = items[index - 1]
                    if (previousSibling instanceof Table) {
                        totalSpacing += previousSibling.margin.bottom
                    }
                }
                break

            case RenderState.HEADER:
                totalSpacing = 0
                break

            case RenderState.FOOTER:
                totalSpacing = node.margin.top
                break
        }

        pointToTwip(totalSpacing)
    }

    void addParagraph(builder, TextBlock paragraph) {

        builder.w.p {
            w.pPr {

                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}")
                }
            }

            String paragraphLinkId = UUID.randomUUID()
            if (paragraph.ref) {
                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.ref) {
                w.bookmarkEnd('w:id': paragraphLinkId)
            }
        }
    }

    protected boolean isStylesEnabled() {
        false
    }

    void addBookmark(builder, Text text) {
        String id = UUID.randomUUID()
        builder.w.bookmarkStart('w:id': id, 'w:name': text.ref)
        addTextRun(builder, text.font as Font, text.value as String)
        builder.w.bookmarkEnd('w:id': id)
    }

    void addLink(builder, Text text) {
        builder.w.hyperlink('w:anchor': text.url[1..-1]) {
            addTextRun(builder, text.font as Font, text.value as String)
        }
    }

    void addLineBreakRun(builder) {
        builder.w.r {
            w.br()
        }
    }

    DocumentPartType getCurrentDocumentPart() {
        switch (renderState) {
            case RenderState.PAGE:
                BasicDocumentPartTypes.DOCUMENT
                break
            case RenderState.HEADER:
                BasicDocumentPartTypes.HEADER
                break
            case RenderState.FOOTER:
                BasicDocumentPartTypes.FOOTER
                break
        }
    }

    void addImageRun(builder, Image image) {
        String blipId = document.element.addImage(image.name, image.data, currentDocumentPart)

        int widthInEmu = pointToEmu(image.width)
        int heightInEmu = pointToEmu(image.height)
        String imageDescription = "Image: ${image.name}"

        builder.w.r {
            w.drawing {
                wp.inline(distT: 0, distR: 0, distB: 0, distL: 0) {
                    wp.extent(cx: widthInEmu, cy: heightInEmu)
                    wp.docPr(id: 1, name: imageDescription, descr: image.name)
                    a.graphic {
                        a.graphicData(uri: 'http://schemas.openxmlformats.org/drawingml/2006/picture') {
                            pic.pic {
                                pic.nvPicPr {
                                    pic.cNvPr(id: 0, name: imageDescription, descr: image.name)
                                    pic.cNvPicPr {
                                        a.picLocks(noChangeAspect: 'true')
                                    }
                                }
                                pic.blipFill {
                                    a.blip('r:embed': blipId)
                                    a.stretch {
                                        a.fillRect()
                                    }
                                }
                                pic.spPr {
                                    a.xfrm {
                                        a.off(x: 0, y: 0)
                                        a.ext(cx: widthInEmu, cy: heightInEmu)
                                    }
                                    a.prstGeom(prst: 'rect') {
                                        a.avLst()
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    void addTable(builder, Table table) {
        builder.w.tbl {
            w.tblPr {
                w.tblW('w:w': pointToTwip(table.width), 'w:type': 'dxa')
                w.tblBorders {
                    def properties = ['top', 'right', 'bottom', 'left', 'insideH', 'insideV']
                    properties.each { String property ->
                        w."${property}"(
                                'w:sz': pointToEigthPoint(table.border.size),
                                'w:color': table.border.color.hex,
                                'w:val': (table.border.size == 0 ? 'none' : 'single')
                        )
                    }
                }
            }

            if (table.columns) {
                w.tblGrid {
                    List columnWidths = table.computeColumnWidths()
                    for (BigDecimal columnWidth in columnWidths) {
                        w.gridCol('w:w': pointToTwip(columnWidth).longValue())
                    }
                }
            }

            table.children.each { Row row ->
                w.tr {
                    row.children.each { Cell column ->
                        if (column.rowsSpanned == 0) {
                            addColumn(builder, column)
                        } else {
                            addMergeColumn(builder)
                        }
                        column.rowsSpanned++
                    }
                }
            }
        }
    }

    void addColumn(builder, Cell column) {
        Table table = column.parent.parent

        builder.w.tc {
            w.tcPr {
                w.vAlign('w:val': 'top')
                w.tcW('w:w': pointToTwip(column.width - (table.padding * 2)), 'w:type': 'dxa')
                w.tcMar {
                    w.top('w:w': pointToTwip(table.padding), 'w:type': 'dxa')
                    w.bottom('w:w': pointToTwip(table.padding), 'w:type': 'dxa')
                    w.left('w:w': pointToTwip(table.padding), 'w:type': 'dxa')
                    w.right('w:w': pointToTwip(table.padding), 'w:type': 'dxa')
                }
                if (column.background) {
                    w.shd('w:val': 'clear', 'w:color': 'auto', 'w:fill': column.background.hex)
                }
                if (column.colspan > 1) {
                    w.gridSpan('w:val': column.colspan)
                }
                if (column.rowspan > 1) {
                    w.vMerge('w:val': 'restart')
                }
            }
            column.children.each {
                if (it instanceof TextBlock) {
                    addParagraph(builder, it)
                } else {
                    addTable(builder, it)
                    w.p()
                }
            }
            if (!column.children) {
                w.p()
            }
        }

    }

    void addMergeColumn(builder) {
        builder.w.tc {
            w.tcPr {
                w.vMerge()
            }
            w.p()
        }
    }

    void addTextRun(builder, Font font, String text) {
        builder.w.r {
            w.rPr {
                w.rFonts('w:ascii': font.family)
                if (font.bold) {
                    w.b()
                }
                if (font.italic) {
                    w.i()
                }
                w.color('w:val': font.color.hex)
                w.sz('w:val': pointToHalfPoint(font.size))
            }
            if (renderState == RenderState.PAGE) {
                w.t(text, RUN_TEXT_OPTIONS)
            } else {
                parseHeaderFooterText(builder, text)
            }
        }
    }

    static void parseHeaderFooterText(builder, String text) {
        def textParts = text.split(PAGE_NUMBER_PLACEHOLDER)
        textParts.eachWithIndex { String part, int index ->
            if (index != 0) {
                builder.w.pgNum()
            }
            builder.w.t(part, RUN_TEXT_OPTIONS)
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy