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.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')
                        }
                    }
                }
            }
        }

        document.element.write()
    }

    def renderHeader(HeaderFooterOptions options) {
        def header = [:]
        if (document.header) {
            renderState = RenderState.HEADER
            header.node = document.header(options)
            header.id = wordDocument.generateHeader { 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.generateFooter { 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 {
                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)
            }
            paragraph.children.each { child ->
                switch (child.getClass()) {
                    case Text:
                        addTextRun(builder, child.font as Font, child.value as String)
                        break
                    case Image:
                        addImageRun(builder, child)
                        break
                    case LineBreak:
                        addLineBreakRun(builder)
                        break
                }
            }
        }
    }

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

    DocumentPartType getCurrentDocumentPart() {
        switch (renderState) {
            case RenderState.PAGE:
                DocumentPartType.DOCUMENT
                break
            case RenderState.HEADER:
                DocumentPartType.HEADER
                break
            case RenderState.FOOTER:
                DocumentPartType.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')
                        )
                    }
                }
            }

            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': 'center')
                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)
            }
        }
    }

    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