From 84c295c08dcfa193b61e36feacab9749d54af663 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Mon, 8 Feb 2016 23:17:03 +0100 Subject: [PATCH 01/12] Upgraded dependencies --- build.gradle | 3 ++- pdf/build.gradle | 4 ++-- word/build.gradle | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) 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/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/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 +} From 97af139a2257ad992e10acf1bb8b8c99b0f4eea2 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Wed, 10 Feb 2016 19:25:29 +0100 Subject: [PATCH 02/12] Image can be loaded from URL --- .../com/craigburke/document/core/Image.groovy | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) 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..7bed3ec 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,45 @@ class Image extends BaseNode { ImageType type = ImageType.JPG Integer width Integer height - byte[] data - - void setType(String value) { - type = Enum.valueOf(ImageType, value.toUpperCase()) + private URL imageUrl + private byte[] imageData + + void setType(String value) { + type = Enum.valueOf(ImageType, value.toUpperCase()) + } + + URL getUrl() { + imageUrl + } + + void setUrl(String value) { + setUrl(value != null ? URI.create(value).toURL() : null) + } + + void setUrl(URL value) { + imageUrl = value + } + + byte[] getData() { + imageUrl?.bytes ?: imageData + } + + void setData(byte[] data) { + imageData = Arrays.copyOf(data, data.length) + } + + def withInputStream(Closure work) { + if(imageUrl != null) { + return imageUrl.withInputStream(work) + } + work.call(new ByteArrayInputStream(imageData)) + } + + String getHash() { + Formatter hexHash = new Formatter() + MessageDigest.getInstance('SHA-1').digest(getData()).each { + b -> hexHash.format('%02x', b) + } + hexHash.toString() } } From 1455a757a01887fb2a7448ffd715c1c16850ff49 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Wed, 10 Feb 2016 19:28:20 +0100 Subject: [PATCH 03/12] If you set only one of width or height, the other is calculated from image --- .../document/core/factory/ImageFactory.groovy | 22 ++++---- .../document/core/BuilderSpec.groovy | 52 +++++++++++++++++++ 2 files changed, 65 insertions(+), 9 deletions(-) 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..c60884e 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 @@ -22,10 +22,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 +54,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/test/groovy/com/craigburke/document/core/BuilderSpec.groovy b/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy index 3b42721..4c3ab6e 100644 --- a/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy +++ b/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy @@ -171,6 +171,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 { From 694939f6e04e83082f2584ead6996447e68f5224 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Wed, 10 Feb 2016 21:11:12 +0100 Subject: [PATCH 04/12] Added cm as short for centimeter --- .../document/core/UnitCategory.groovy | 1 + .../craigburke/document/core/UnitUtil.groovy | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) 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 + } } From 8658d09e649b3e18c2c80c17fbaae8384fd29bd8 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Wed, 10 Feb 2016 21:12:18 +0100 Subject: [PATCH 05/12] You can now specify standard paper sizes Letter, Legal, A4, A5, etc. And also orientation portrait/landscape. --- .../craigburke/document/core/Dimension.groovy | 12 +++++ .../craigburke/document/core/Document.groovy | 44 +++++++++++++++++- .../craigburke/document/core/PaperSize.groovy | 39 ++++++++++++++++ .../document/core/BuilderSpec.groovy | 45 +++++++++++++++++++ .../builder/WordDocumentBuilder.groovy | 2 +- 5 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 core/src/main/groovy/com/craigburke/document/core/Dimension.groovy create mode 100644 core/src/main/groovy/com/craigburke/document/core/PaperSize.groovy 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..c1c36b6 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,40 @@ class Document extends BlockNode { List children = [] List embeddedFonts = [] + /** + * Set width and height of the document. + * + * @param arg a Dimension instance, a List [width, height] or the name of a standard paper size ("a4", "letter", "legal") + */ + void setSize(def arg) { + if (arg instanceof Dimension) { + width = inchToPoint(arg.width) + height = inchToPoint(arg.height) + } else if (arg instanceof List && arg.size() == 2) { + width = inchToPoint(arg[0]) + height = inchToPoint(arg[1]) + } else { + def size = PaperSize.get(arg.toString()) + width = inchToPoint(size.width) + height = inchToPoint(size.height) + } + } + + /** + * 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 + } + } } 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/test/groovy/com/craigburke/document/core/BuilderSpec.groovy b/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy index 3b42721..7ae826c 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,51 @@ 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 "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 { 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..adbce95 100644 --- a/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy +++ b/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy @@ -67,7 +67,7 @@ class WordDocumentBuilder extends DocumentBuilder { 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), From b4faf2b843e5b8603a675efc20e0bfecb793954c Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Wed, 10 Feb 2016 23:25:54 +0100 Subject: [PATCH 06/12] Setting page size and orientation now works with PDF --- .../groovy/com/craigburke/document/core/Document.groovy | 4 ++++ .../com/craigburke/document/builder/PdfDocument.groovy | 9 +++++++++ 2 files changed, 13 insertions(+) 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 c1c36b6..2112216 100644 --- a/core/src/main/groovy/com/craigburke/document/core/Document.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/Document.groovy @@ -81,4 +81,8 @@ class Document extends BlockNode { height = tmp } } + + boolean isLandscape() { + this.orientation == LANDSCAPE + } } 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++ From 68c802a97eddee01f4e6640d0225eec7e0a3b109 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Wed, 10 Feb 2016 23:30:45 +0100 Subject: [PATCH 07/12] Unused import --- .../com/craigburke/document/core/factory/ImageFactory.groovy | 1 - 1 file changed, 1 deletion(-) 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 c60884e..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 From da69881ff578ea9c0099df0de8cfe8a2b4542c19 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Thu, 11 Feb 2016 19:22:02 +0100 Subject: [PATCH 08/12] Simplified things by storing url as a String instead of URL instance --- .../com/craigburke/document/core/Image.groovy | 30 +++++-------------- 1 file changed, 7 insertions(+), 23 deletions(-) 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 7bed3ec..8440f09 100644 --- a/core/src/main/groovy/com/craigburke/document/core/Image.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/Image.groovy @@ -11,38 +11,22 @@ class Image extends BaseNode { ImageType type = ImageType.JPG Integer width Integer height - private URL imageUrl - private byte[] imageData + String url + byte[] data void setType(String value) { type = Enum.valueOf(ImageType, value.toUpperCase()) } - URL getUrl() { - imageUrl - } - - void setUrl(String value) { - setUrl(value != null ? URI.create(value).toURL() : null) - } - - void setUrl(URL value) { - imageUrl = value - } - byte[] getData() { - imageUrl?.bytes ?: imageData - } - - void setData(byte[] data) { - imageData = Arrays.copyOf(data, data.length) + if(this.@data == null && url != null) { + this.data = new URL(url).bytes + } + this.@data } def withInputStream(Closure work) { - if(imageUrl != null) { - return imageUrl.withInputStream(work) - } - work.call(new ByteArrayInputStream(imageData)) + work.call(new ByteArrayInputStream(getData())) } String getHash() { From 3ce05a75e0363ad6a7d0be13dc615b18a0dc9a82 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Thu, 11 Feb 2016 19:46:38 +0100 Subject: [PATCH 09/12] Refactored setSize() into three separate methods --- .../craigburke/document/core/Document.groovy | 36 ++++++++++++------- .../document/core/BuilderSpec.groovy | 15 ++++++++ 2 files changed, 38 insertions(+), 13 deletions(-) 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 2112216..bd535cc 100644 --- a/core/src/main/groovy/com/craigburke/document/core/Document.groovy +++ b/core/src/main/groovy/com/craigburke/document/core/Document.groovy @@ -48,20 +48,30 @@ class Document extends BlockNode { /** * Set width and height of the document. * - * @param arg a Dimension instance, a List [width, height] or the name of a standard paper size ("a4", "letter", "legal") + * @param arg name of a standard paper size ("a4", "letter", "legal") */ - void setSize(def arg) { - if (arg instanceof Dimension) { - width = inchToPoint(arg.width) - height = inchToPoint(arg.height) - } else if (arg instanceof List && arg.size() == 2) { - width = inchToPoint(arg[0]) - height = inchToPoint(arg[1]) - } else { - def size = PaperSize.get(arg.toString()) - width = inchToPoint(size.width) - height = inchToPoint(size.height) - } + 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] } /** 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 7ae826c..682e161 100644 --- a/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy +++ b/core/src/test/groovy/com/craigburke/document/core/BuilderSpec.groovy @@ -80,6 +80,21 @@ class BuilderSpec extends Specification { 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 { From aa5576e43766113e7cd464014756d1f5170bf5ce Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Tue, 16 Feb 2016 21:19:16 +0100 Subject: [PATCH 10/12] Implemented Table of Contents --- .../com/craigburke/document/core/Toc.groovy | 10 ++++++++ .../core/builder/DocumentBuilder.groovy | 2 ++ .../document/core/factory/TocFactory.groovy | 18 +++++++++++++ .../builder/WordDocumentBuilder.groovy | 25 +++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 core/src/main/groovy/com/craigburke/document/core/Toc.groovy create mode 100644 core/src/main/groovy/com/craigburke/document/core/factory/TocFactory.groovy 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/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/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/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy b/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy index 8640ce8..dee32e0 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 @@ -62,6 +63,8 @@ class WordDocumentBuilder extends DocumentBuilder { addPageBreak(builder) } else if (child instanceof Table) { addTable(builder, child) + } else if (child instanceof Toc) { + addTableOfContents(builder) } } w.sectPr { @@ -128,6 +131,28 @@ class WordDocumentBuilder extends DocumentBuilder { } + void addTableOfContents(builder) { + builder.w.p { + w.r { + w.fldChar('w:fldCharType': 'begin') + } + w.r { + w.instrText('xml:space': 'preserve') { + mkp.yieldUnescaped('TOC \\* MERGEFORMAT') + } + } + w.r { + w.fldChar('w:fldCharType': 'separate') + } + w.r { + mkp.yieldUnescaped('...') + } + w.r { + w.fldChar('w:fldCharType': 'end') + } + } + } + void addPageBreak(builder) { builder.w.p { w.r { From bf952ab3219acf5e0476de0f7dff47c18a263459 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Tue, 16 Feb 2016 21:19:16 +0100 Subject: [PATCH 11/12] Implemented Table of Contents --- .../com/craigburke/document/core/Toc.groovy | 10 ++++++++ .../core/builder/DocumentBuilder.groovy | 2 ++ .../document/core/factory/TocFactory.groovy | 18 +++++++++++++ .../builder/WordDocumentBuilder.groovy | 25 +++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 core/src/main/groovy/com/craigburke/document/core/Toc.groovy create mode 100644 core/src/main/groovy/com/craigburke/document/core/factory/TocFactory.groovy 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/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/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/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy b/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy index adbce95..45e36f5 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 @@ -62,6 +63,8 @@ class WordDocumentBuilder extends DocumentBuilder { addPageBreak(builder) } else if (child instanceof Table) { addTable(builder, child) + } else if (child instanceof Toc) { + addTableOfContents(builder) } } w.sectPr { @@ -128,6 +131,28 @@ class WordDocumentBuilder extends DocumentBuilder { } + void addTableOfContents(builder) { + builder.w.p { + w.r { + w.fldChar('w:fldCharType': 'begin') + } + w.r { + w.instrText('xml:space': 'preserve') { + mkp.yieldUnescaped('TOC \\* MERGEFORMAT') + } + } + w.r { + w.fldChar('w:fldCharType': 'separate') + } + w.r { + mkp.yieldUnescaped('...') + } + w.r { + w.fldChar('w:fldCharType': 'end') + } + } + } + void addPageBreak(builder) { builder.w.p { w.r { From ff5a7fc0e8cdffb8f6081ab7c0737a515cce9c63 Mon Sep 17 00:00:00 2001 From: Goran Ehrsson Date: Thu, 18 Feb 2016 09:29:38 +0100 Subject: [PATCH 12/12] Styles WIP --- .../builder/WordDocumentBuilder.groovy | 158 +++++++++++++----- 1 file changed, 119 insertions(+), 39 deletions(-) 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 45e36f5..f2c18ee 100644 --- a/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy +++ b/word/src/main/groovy/com/craigburke/document/builder/WordDocumentBuilder.groovy @@ -34,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) } @@ -49,6 +51,7 @@ class WordDocumentBuilder extends DocumentBuilder { dateGenerated: new Date() ) + renderStyles([:]) def header = renderHeader(headerFooterOptions) def footer = renderFooter(headerFooterOptions) @@ -131,21 +134,91 @@ 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 \\* MERGEFORMAT') + mkp.yieldUnescaped('TOC \\o "1-3" \\h \\z \\u') } } w.r { w.fldChar('w:fldCharType': 'separate') } w.r { - mkp.yieldUnescaped('...') + w.rPr { + w.noProof() + } + w.t { + mkp.yieldUnescaped('RIGHT-CLICK TO UPDATE FIELD.') + } } w.r { w.fldChar('w:fldCharType': 'end') @@ -224,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) } } @@ -253,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) { @@ -278,7 +358,7 @@ class WordDocumentBuilder extends DocumentBuilder { } protected boolean isStylesEnabled() { - false + true } void addBookmark(builder, Text text) {