diff --git a/gradle.properties b/gradle.properties index dc8fb5a..a21f623 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ group = org.xbib.graphics name = graphics -version = 4.0.0 +version = 4.0.1 gradle.wrapper.version = 6.6.1 pdfbox.version = 2.0.22 @@ -10,4 +10,9 @@ reflections.version = 0.9.11 jfreechart.version = 1.5.1 batik.version = 1.13 junit.version = 5.7.1 -junit4.version = 4.13 \ No newline at end of file +junit4.version = 4.13 +groovy.version = 2.5.12 +spock.version = 1.3-groovy-2.5 +cglib.version = 3.2.5 +objenesis.version = 2.6 +log4j.version = 2.14.0 diff --git a/gradle/compile/groovy.gradle b/gradle/compile/groovy.gradle new file mode 100644 index 0000000..1abf883 --- /dev/null +++ b/gradle/compile/groovy.gradle @@ -0,0 +1,34 @@ +apply plugin: 'groovy' + +dependencies { + implementation "org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy" +} + +compileGroovy { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +compileTestGroovy { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 +} + +tasks.withType(GroovyCompile) { + options.compilerArgs + if (!options.compilerArgs.contains("-processor")) { + options.compilerArgs << '-proc:none' + } + groovyOptions.optimizationOptions.indy = true +} + +task groovydocJar(type: Jar, dependsOn: 'groovydoc') { + from groovydoc.destinationDir + archiveClassifier.set('javadoc') +} + +configurations.all { + resolutionStrategy { + force "org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy" + } +} diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle index fff776d..0454148 100644 --- a/gradle/test/junit5.gradle +++ b/gradle/test/junit5.gradle @@ -13,7 +13,6 @@ test { useJUnitPlatform() failFast = false systemProperty 'java.awt.headless', 'true' - //-Dawt.toolkit=sun.awt.HToolkit testLogging { events 'STARTED', 'PASSED', 'FAILED', 'SKIPPED' showStandardStreams = true diff --git a/graphics-barcode/src/main/java/org/xbib/graphics/barcode/render/GraphicsRenderer.java b/graphics-barcode/src/main/java/org/xbib/graphics/barcode/render/GraphicsRenderer.java index 1b96fad..285cf7b 100755 --- a/graphics-barcode/src/main/java/org/xbib/graphics/barcode/render/GraphicsRenderer.java +++ b/graphics-barcode/src/main/java/org/xbib/graphics/barcode/render/GraphicsRenderer.java @@ -53,10 +53,12 @@ public class GraphicsRenderer { * Creates a new Java 2D renderer. * * @param g2d the graphics to render to + * @param rectangle the visible rectangle * @param scalingFactor the scaling factor to apply * @param background the paper (background) color * @param foreground the ink (foreground) color * @param antialias if true give anti alias hint + * @param transparentBackground if true background should be transparent */ public GraphicsRenderer(Graphics2D g2d, Rectangle rectangle, diff --git a/graphics-pdfbox-groovy/LICENSE.txt b/graphics-pdfbox-groovy/LICENSE.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/graphics-pdfbox-groovy/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/graphics-pdfbox-groovy/NOTICE.txt b/graphics-pdfbox-groovy/NOTICE.txt new file mode 100644 index 0000000..902188c --- /dev/null +++ b/graphics-pdfbox-groovy/NOTICE.txt @@ -0,0 +1,8 @@ + +This is a modified version of Craig Burke's document-builder at + +https://github.com/craigburke/document-builder + +extended by a `DocumentAnalyzer` which can examine PDF documents by using Apache PdfBox https://pdfbox.apache.org + +This library will be extended by useful PDF classes with an emphasis on Groovy. diff --git a/graphics-pdfbox-groovy/build.gradle b/graphics-pdfbox-groovy/build.gradle new file mode 100644 index 0000000..4a232ec --- /dev/null +++ b/graphics-pdfbox-groovy/build.gradle @@ -0,0 +1,52 @@ + +apply from: rootProject.file('gradle/compile/groovy.gradle') + +dependencies { + api project(':graphics-pdfbox') + api project(':graphics-barcode') + api("org.codehaus.groovy:groovy-xml:${project.property('groovy.version')}:indy") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + // in groovyland, we need log4j-core for @Log4j2 annotations + implementation "org.apache.logging.log4j:log4j-core:${project.property('log4j.version')}" + + // spock need junit vintage + testRuntimeOnly "org.junit.vintage:junit-vintage-engine:${project.property('junit.version')}" + + testImplementation("org.codehaus.groovy:groovy:${project.property('groovy.version')}:indy") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + testImplementation("org.codehaus.groovy:groovy-json:${project.property('groovy.version')}:indy") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + testImplementation("org.codehaus.groovy:groovy-nio:${project.property('groovy.version')}:indy"){ + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + testImplementation("org.codehaus.groovy:groovy-sql:${project.property('groovy.version')}:indy") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + testImplementation("org.codehaus.groovy:groovy-xml:${project.property('groovy.version')}:indy") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + testImplementation("org.codehaus.groovy:groovy-macro:${project.property('groovy.version')}:indy") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + testImplementation("org.codehaus.groovy:groovy-templates:${project.property('groovy.version')}:indy") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + testImplementation("org.codehaus.groovy:groovy-test:${project.property('groovy.version')}:indy") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + } + testImplementation("org.spockframework:spock-core:${project.property('spock.version')}") { + exclude group: 'org.codehaus.groovy', module: 'groovy' + exclude group: 'org.codehaus.groovy', module: 'groovy-json' + exclude group: 'org.codehaus.groovy', module: 'groovy-macro' + exclude group: 'org.codehaus.groovy', module: 'groovy-nio' + exclude group: 'org.codehaus.groovy', module: 'groovy-sql' + exclude group: 'org.codehaus.groovy', module: 'groovy-templates' + exclude group: 'org.codehaus.groovy', module: 'groovy-test' + exclude group: 'org.codehaus.groovy', module: 'groovy-xml' + } + testImplementation "cglib:cglib-nodep:${project.property('cglib.version')}" // for spock mock + testImplementation "org.objenesis:objenesis:${project.property('objenesis.version')}" // for spock mock +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Align.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Align.groovy new file mode 100644 index 0000000..04aeace --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Align.groovy @@ -0,0 +1,13 @@ +package org.xbib.graphics.pdfbox.groovy + +enum Align { + LEFT('left'), + RIGHT('right'), + CENTER('center') + + String value + + Align(String value) { + this.value = value + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Alignable.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Alignable.groovy new file mode 100644 index 0000000..9e447b5 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Alignable.groovy @@ -0,0 +1,9 @@ +package org.xbib.graphics.pdfbox.groovy + +trait Alignable { + Align align = Align.LEFT + + void setAlign(String value) { + align = Enum.valueOf(Align, value.toUpperCase()) + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BackgroundAssignable.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BackgroundAssignable.groovy new file mode 100644 index 0000000..a66cb5a --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BackgroundAssignable.groovy @@ -0,0 +1,12 @@ +package org.xbib.graphics.pdfbox.groovy + +trait BackgroundAssignable { + Color background + + void setBackground(String value) { + if (value) { + background = background ?: new Color() + background.color = value + } + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Barcode.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Barcode.groovy new file mode 100644 index 0000000..bae6bd1 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Barcode.groovy @@ -0,0 +1,20 @@ +package org.xbib.graphics.pdfbox.groovy + +class Barcode extends BaseNode { + + Integer x = 0 + + Integer y = 0 + + Integer width = 0 + + Integer height = 0 + + String value + + BarcodeType type + + void setType(String type) { + this.type = Enum.valueOf(BarcodeType, type.toUpperCase()) + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BarcodeType.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BarcodeType.groovy new file mode 100644 index 0000000..7660545 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BarcodeType.groovy @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.groovy + +enum BarcodeType { + CODE39 +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BaseNode.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BaseNode.groovy new file mode 100644 index 0000000..877e7a0 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BaseNode.groovy @@ -0,0 +1,8 @@ +package org.xbib.graphics.pdfbox.groovy + +class BaseNode { + + def element + + def parent +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BlockNode.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BlockNode.groovy new file mode 100644 index 0000000..08deee7 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/BlockNode.groovy @@ -0,0 +1,10 @@ +package org.xbib.graphics.pdfbox.groovy + +class BlockNode extends BaseNode implements Stylable, Alignable { + + static Margin defaultMargin = new Margin(top: 0, bottom: 0, left: 0, right: 0) + + Margin margin = new Margin() + + Border border = new Border() +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Bookmarkable.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Bookmarkable.groovy new file mode 100644 index 0000000..15830e2 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Bookmarkable.groovy @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.groovy + +trait Bookmarkable { + String ref +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Border.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Border.groovy new file mode 100644 index 0000000..95b46f7 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Border.groovy @@ -0,0 +1,9 @@ +package org.xbib.graphics.pdfbox.groovy + +class Border implements ColorAssignable { + Integer size = 1 + + def leftShift(Map properties) { + properties?.each { key, value -> this[key] = value } + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Cell.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Cell.groovy new file mode 100644 index 0000000..2ff4c00 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Cell.groovy @@ -0,0 +1,16 @@ +package org.xbib.graphics.pdfbox.groovy + +class Cell extends BlockNode implements Stylable, Alignable, BackgroundAssignable { + + List children = [] + + Integer width = 0 + + Integer colspan = 1 + + Integer rowspan = 1 + + Integer rowsSpanned = 0 + + BigDecimal rowspanHeight = 0 +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Color.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Color.groovy new file mode 100644 index 0000000..0fb00a1 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Color.groovy @@ -0,0 +1,26 @@ +package org.xbib.graphics.pdfbox.groovy + +import groovy.transform.AutoClone + +@AutoClone +class Color { + String hex = '000000' + def rgb = [0, 0, 0] + + void setColor(String value) { + if (value.startsWith('#')) { + String hexString = value[1..-1] + this.hex = hexString + this.rgb = (hexString =~ /.{2}/).collect { Integer.parseInt(it, 16) } + } + } + + private setHex(String value) { + throw new UnsupportedOperationException("Cannot directly set hex to ${value}, use the color property") + } + + private setRgb(value) { + throw new UnsupportedOperationException("Cannot directly set rgb to ${value}, use the color property") + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/ColorAssignable.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/ColorAssignable.groovy new file mode 100644 index 0000000..f5a1208 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/ColorAssignable.groovy @@ -0,0 +1,9 @@ +package org.xbib.graphics.pdfbox.groovy + +trait ColorAssignable { + Color color = new Color() + + void setColor(String value) { + color.color = value + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Document.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Document.groovy new file mode 100644 index 0000000..38a6c8c --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Document.groovy @@ -0,0 +1,52 @@ +package org.xbib.graphics.pdfbox.groovy + +class Document extends BlockNode { + + static final Margin defaultMargin = new Margin(top: UnitUtil.mmToPoint(5 as BigDecimal), + bottom: UnitUtil.mmToPoint(5 as BigDecimal), + left: UnitUtil.mmToPoint(5 as BigDecimal), + right: UnitUtil.mmToPoint(5 as BigDecimal)) + + static final BigDecimal defaultWidth = UnitUtil.mmToPoint(210 as BigDecimal) + + static final BigDecimal defaultHeight = UnitUtil.mmToPoint(297 as BigDecimal) + + String papersize = 'A4' + + String orientation = 'portrait' + + BigDecimal width = defaultWidth + + BigDecimal height = defaultHeight + + def template + + def header + + def footer + + private Map templateMap + + List children = [] + + List fonts = [] + + Map getTemplateMap() { + if (templateMap == null) { + loadTemplateMap() + } + templateMap + } + + private void loadTemplateMap() { + templateMap = [:] + if (template && template instanceof Closure) { + def templateDelegate = new Expando() + templateDelegate.metaClass.methodMissing = { name, args -> + templateMap[name] = args[0] + } + template.delegate = templateDelegate + template() + } + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Font.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Font.groovy new file mode 100644 index 0000000..78aa1cd --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Font.groovy @@ -0,0 +1,22 @@ +package org.xbib.graphics.pdfbox.groovy + +class Font implements ColorAssignable, Cloneable { + + String family = 'Helvetica' + + BigDecimal size = 12 + + boolean bold = false + + boolean italic = false + + def leftShift(Map properties) { + properties?.each { key, value -> this[key] = value } + } + + Object clone() { + Font result = new Font(family: family, size: size, bold: bold, italic: italic) + result.color = "#${color.hex}" + result + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/HeaderFooterOptions.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/HeaderFooterOptions.groovy new file mode 100644 index 0000000..61968ee --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/HeaderFooterOptions.groovy @@ -0,0 +1,10 @@ +package org.xbib.graphics.pdfbox.groovy + +class HeaderFooterOptions { + + Date dateGenerated + + String pageCount + + String pageNumber +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Heading.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Heading.groovy new file mode 100644 index 0000000..9439427 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Heading.groovy @@ -0,0 +1,8 @@ +package org.xbib.graphics.pdfbox.groovy + +class Heading extends TextBlock implements Linkable { + + static final FONT_SIZE_MULTIPLIERS = [2, 1.5, 1.17, 1.12, 0.83, 0.75] + + int level = 1 +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Image.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Image.groovy new file mode 100644 index 0000000..e21f89d --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Image.groovy @@ -0,0 +1,22 @@ +package org.xbib.graphics.pdfbox.groovy + +class Image extends BaseNode { + + String name + + ImageType type + + Integer x = 0 + + Integer y = 0 + + Integer width + + Integer height + + byte[] data + + void setType(String value) { + type = Enum.valueOf(ImageType, value.toUpperCase()) + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/ImageType.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/ImageType.groovy new file mode 100644 index 0000000..7c0e199 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/ImageType.groovy @@ -0,0 +1,15 @@ +package org.xbib.graphics.pdfbox.groovy + +enum ImageType { + PNG('png'), + JPG('jpg'), + TIF('tif'), + GIF('gif'), + BMP('bmp') + + String value + + ImageType(String value) { + this.value = value + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Line.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Line.groovy new file mode 100644 index 0000000..3390458 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Line.groovy @@ -0,0 +1,17 @@ +package org.xbib.graphics.pdfbox.groovy + +import groovy.transform.AutoClone + +@AutoClone +class Line extends BaseNode { + + Integer startX = 0 + + Integer startY = 0 + + Integer endX = 0 + + Integer endY = 0 + + float strokewidth = 0.0f +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/LineBreak.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/LineBreak.groovy new file mode 100644 index 0000000..f4f7c61 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/LineBreak.groovy @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.groovy + +class LineBreak extends BaseNode { + Integer height = 0 +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Link.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Link.groovy new file mode 100644 index 0000000..3114680 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Link.groovy @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.groovy + +class Link extends Text { + String url +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Linkable.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Linkable.groovy new file mode 100644 index 0000000..9e8e136 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Linkable.groovy @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.groovy + +trait Linkable { + String url +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Margin.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Margin.groovy new file mode 100644 index 0000000..2842d68 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Margin.groovy @@ -0,0 +1,29 @@ +package org.xbib.graphics.pdfbox.groovy + +import groovy.transform.AutoClone + +@AutoClone +class Margin { + + static final Margin NONE = new Margin(top: 0, right: 0, bottom: 0, left: 0) + + Integer top + + Integer bottom + + Integer left + + Integer right + + void setDefaults(Margin defaultMargin) { + top = (top == null) ? defaultMargin.top : top + bottom = (bottom == null) ? defaultMargin.bottom : bottom + left = (left == null) ? defaultMargin.left : left + right = (right == null) ? defaultMargin.right : right + } + + def leftShift(Map properties) { + properties?.each { key, value -> this[key] = value } + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/PageBreak.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/PageBreak.groovy new file mode 100644 index 0000000..6714674 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/PageBreak.groovy @@ -0,0 +1,4 @@ +package org.xbib.graphics.pdfbox.groovy + +class PageBreak extends BaseNode { +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Row.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Row.groovy new file mode 100644 index 0000000..c1079cf --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Row.groovy @@ -0,0 +1,8 @@ +package org.xbib.graphics.pdfbox.groovy + +class Row extends BaseNode implements Stylable, BackgroundAssignable { + + List children = [] + + Integer width +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Stylable.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Stylable.groovy new file mode 100644 index 0000000..399052d --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Stylable.groovy @@ -0,0 +1,8 @@ +package org.xbib.graphics.pdfbox.groovy + +trait Stylable { + + Font font + + String style +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Table.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Table.groovy new file mode 100644 index 0000000..c64fa55 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Table.groovy @@ -0,0 +1,92 @@ +package org.xbib.graphics.pdfbox.groovy + + +import java.math.RoundingMode + +class Table extends BlockNode implements BackgroundAssignable { + + static Margin defaultMargin = new Margin(top: 0, bottom: 0, left: 0, right: 0) + + List children = [] + + Integer padding = 10 + + Integer width = 0 + + List columns = [] + + int getColumnCount() { + if (columns) { + columns.size() + } else { + (children) ? children.max { it.children.size() }.children.size() : 0 + } + } + + void normalizeColumnWidths() { + updateRowspanColumns() + width = Math.min(width ?: maxWidth, maxWidth) + if (!columns) { + columnCount.times { columns << (1 as BigDecimal) } + } + List columnWidths = computeColumnWidths() + children.each { row -> + int columnWidthIndex = 0 + row.children.eachWithIndex { column, index -> + int endIndex = columnWidthIndex + column.colspan - 1 + BigDecimal missingBorderWidth = (column.colspan - 1) * border.size + column.width = columnWidths[columnWidthIndex..endIndex].sum() + missingBorderWidth + columnWidthIndex += column.colspan + column.children.findAll { it instanceof Table }.each { it.normalizeColumnWidths() } + } + } + } + + List computeColumnWidths() { + + BigDecimal relativeTotal = columns.sum() as BigDecimal + + BigDecimal totalBorderWidth = (columnCount + 1) * border.size + + BigDecimal totalCellWidth = width - totalBorderWidth + + List columnWidths = [] + + columns.eachWithIndex { column, index -> + if (index == columns.size() - 1) { + columnWidths << ((totalCellWidth - (columnWidths.sum() as BigDecimal ?: 0)) as BigDecimal) + } else { + BigDecimal d = (columns[index] / relativeTotal) * totalCellWidth + columnWidths << d.setScale(0, RoundingMode.CEILING) + } + } + columnWidths + } + + void updateRowspanColumns() { + def updatedColumns = [] + children.eachWithIndex { row, rowIndex -> + row.children.eachWithIndex { column, columnIndex -> + if (column.rowspan > 1 && !updatedColumns.contains(column)) { + int rowspanEnd = Math.min(children.size() - 1, rowIndex + column.rowspan - 1) + (rowIndex + 1..rowspanEnd).each { + children[it].children.addAll(columnIndex, [column]) + } + updatedColumns << column + } + } + } + } + + private int getMaxWidth() { + if (parent instanceof Document) { + parent.width - parent.margin.left - parent.margin.right + } else if (parent instanceof Cell) { + Table outerTable = parent.parent.parent + parent.width - (outerTable.padding * 2) + } else { + 0 + } + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Text.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Text.groovy new file mode 100644 index 0000000..7f1dab2 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/Text.groovy @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.groovy + +class Text extends BaseNode implements Stylable, Linkable, Bookmarkable { + String value +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/TextBlock.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/TextBlock.groovy new file mode 100644 index 0000000..915739c --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/TextBlock.groovy @@ -0,0 +1,31 @@ +package org.xbib.graphics.pdfbox.groovy + +import groovy.transform.AutoClone + +@AutoClone +class TextBlock extends BlockNode implements Linkable, Bookmarkable { + + BigDecimal heightfactor + + List children = [] + + String getText() { + children.findAll { it.getClass() == Text }*.value.join('') + } + + List addText(String text) { + List elements = [] + def textSections = text.split('\n') + textSections.each { String section -> + elements << new Text(value: section, parent: this) + if (section != textSections.last()) { + elements << new LineBreak(parent: this) + } + } + if (text.endsWith('\n')) { + elements << new LineBreak(parent: this) + } + children += elements + elements + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/UnitCategory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/UnitCategory.groovy new file mode 100644 index 0000000..60cc714 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/UnitCategory.groovy @@ -0,0 +1,17 @@ +package org.xbib.graphics.pdfbox.groovy + +@Category(Number) +class UnitCategory { + + BigDecimal getCm() { this * UnitUtil.DPI / UnitUtil.CM_INCH } + + BigDecimal getMm() { this * UnitUtil.DPI / UnitUtil.MM_INCH } + + BigDecimal getInches() { this * UnitUtil.DPI } + + BigDecimal getInch() { this * UnitUtil.DPI } + + BigDecimal getPt() { this } + + BigDecimal getPx() { this } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/UnitUtil.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/UnitUtil.groovy new file mode 100644 index 0000000..20981b8 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/UnitUtil.groovy @@ -0,0 +1,85 @@ +package org.xbib.graphics.pdfbox.groovy + +class UnitUtil { + + static final BigDecimal DPI = 72 + + static final BigDecimal MM_INCH = 25.4 + + static final BigDecimal CM_INCH = 2.54 + + static final BigDecimal PICA_POINTS = 6 + + static final BigDecimal TWIP_POINTS = 20 + + static final BigDecimal EIGTH_POINTS = 8 + + static final BigDecimal HALF_POINTS = 2 + + static final BigDecimal EMU_POINTS = 12700 + + static BigDecimal mmToPoint(BigDecimal mm) { + mm * DPI / MM_INCH + } + + static BigDecimal cmToPoint(BigDecimal cm) { + cm * DPI / CM_INCH + } + + static BigDecimal pointToMm(BigDecimal point) { + point / DPI * MM_INCH + } + + static BigDecimal pointToCm(BigDecimal point) { + point / DPI * CM_INCH + } + + static BigDecimal inchToPoint(BigDecimal inch) { + inch * DPI + } + + static BigDecimal pointToInch(BigDecimal point) { + point / DPI + } + + static BigDecimal pointToPica(BigDecimal point) { + point * PICA_POINTS + } + + static BigDecimal picaToPoint(BigDecimal pica) { + pica / PICA_POINTS + } + + static BigDecimal pointToEigthPoint(BigDecimal point) { + point * EIGTH_POINTS + } + + static BigDecimal eightPointToPoint(BigDecimal eigthPoint) { + eigthPoint / EIGTH_POINTS + } + + static BigDecimal pointToHalfPoint(BigDecimal point) { + point * HALF_POINTS + } + + static BigDecimal halfPointToPoint(BigDecimal halfPoint) { + halfPoint / HALF_POINTS + } + + static BigDecimal pointToTwip(BigDecimal point) { + point * TWIP_POINTS + } + + static BigDecimal twipToPoint(BigDecimal twip) { + twip / TWIP_POINTS + } + + static BigDecimal pointToEmu(BigDecimal point) { + point * EMU_POINTS + } + + static BigDecimal emuToPoint(BigDecimal emu) { + emu / EMU_POINTS + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/analyze/DocumentAnalyzer.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/analyze/DocumentAnalyzer.groovy new file mode 100644 index 0000000..2feae14 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/analyze/DocumentAnalyzer.groovy @@ -0,0 +1,191 @@ +package org.xbib.graphics.pdfbox.groovy.analyze + +import groovy.util.logging.Log4j2 +import org.apache.logging.log4j.Level +import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine +import org.apache.pdfbox.cos.COSName +import org.apache.pdfbox.cos.COSStream +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.pdmodel.PDPage +import org.apache.pdfbox.pdmodel.PDResources +import org.apache.pdfbox.pdmodel.font.PDFont +import org.apache.pdfbox.pdmodel.graphics.image.PDImage +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject + +import java.awt.geom.Point2D + +@Log4j2 +class DocumentAnalyzer { + + private final Map result = [:] + + private final Set seen = new HashSet<>() + + DocumentAnalyzer(InputStream inputStream) { + inputStream.withCloseable { + PDDocument document = PDDocument.load(inputStream) + result."author" = document.getDocumentInformation().author + result."creator" = document.getDocumentInformation().creator + result."producer" = document.getDocumentInformation().producer + result."title" = document.getDocumentInformation().title + result."pagecount" = document.getNumberOfPages() + try { + result."creationDate" = document.getDocumentInformation().creationDate?.toInstant() + result."modificationDate" = document.getDocumentInformation().modificationDate?.toInstant() + } catch (Exception e) { + // NPE if creation/modification dates are borked + /** + * java.lang.NullPointerException: null + * at java.text.SimpleDateFormat.matchZoneString(SimpleDateFormat.java:1695) ~[?:?] + * at java.text.SimpleDateFormat.subParseZoneString(SimpleDateFormat.java:1763) ~[?:?] + * at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:2169) ~[?:?] + * at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1541) ~[?:?] + * at org.apache.pdfbox.util.DateConverter.parseSimpleDate(DateConverter.java:587) ~[pdfbox-2.0.12.jar:2.0.12] + * at org.apache.pdfbox.util.DateConverter.parseDate(DateConverter.java:658) ~[pdfbox-2.0.12.jar:2.0.12] + * at org.apache.pdfbox.util.DateConverter.toCalendar(DateConverter.java:723) ~[pdfbox-2.0.12.jar:2.0.12] + * at org.apache.pdfbox.util.DateConverter.toCalendar(DateConverter.java:701) ~[pdfbox-2.0.12.jar:2.0.12] + * at org.apache.pdfbox.cos.COSDictionary.getDate(COSDictionary.java:790) ~[pdfbox-2.0.12.jar:2.0.12] + * at org.apache.pdfbox.pdmodel.PDDocumentInformation.getCreationDate(PDDocumentInformation.java:212) ~[pdfbox-2.0.12.jar:2.0.12] + */ + log.log(Level.WARN, e.getMessage() as String, e) + } + result."pages" = [] + document.withCloseable { + int images = 0 + int pagecount = result."pagecount" as int + for (int i = 0; i < pagecount; i++) { + PDPage pdPage = document.getPage(i) + Map pageMap = analyze(i, pdPage) + images += pageMap."images".size() + result."pages" << pageMap + } + result."imagecount" = images + } + } + } + + Map getResult() { + result + } + + Map analyze(int i, PDPage page) { + def m = [:] + m."page" = i + m."bbox" = [height: page.getBBox().getHeight(), width: page.getBBox().getWidth()] + m."cropbox" = [height: page.getCropBox().getHeight(), width: page.getCropBox().getWidth()] + m."mediabox" = [height: page.getMediaBox().getHeight(), width: page.getMediaBox().getWidth()] + m."bleedbox" = [height: page.getBleedBox().getHeight(), width: page.getBleedBox().getWidth()] + m."rotation" = page.getRotation() + m."images" = [] + ImageGraphicsExtractor extractor = new ImageGraphicsExtractor(m."images" as List, page) + extractor.process() + m."fonts" = [] + PDResources res = page.getResources() + for (COSName cosName : res.getFontNames()) { + PDFont font = res.getFont(cosName) + if (font) { + def f = [:] + f."name" = font.name + f."damaged" = font.damaged + f."embedded" = font.embedded + f."type" = font.type + f."subtype" = font.subType + m."fonts" << f + } + } + m + } + + class ImageGraphicsExtractor extends PDFGraphicsStreamEngine { + + private final List list + + protected ImageGraphicsExtractor(List list, PDPage page) { + super(page) + this.list = list + } + + void process() throws IOException { + processPage(getPage()) + } + + @Override + void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException { + + } + + @Override + void drawImage(PDImage pdImage) throws IOException { + if (pdImage instanceof PDImageXObject) { + PDImageXObject xobject = pdImage as PDImageXObject + if (seen.contains(xobject.getCOSObject())) { + // skip duplicate image + return + } + seen.add(xobject.getCOSObject()) + def m = [:] + m."width" = xobject.width + m."height" = xobject.height + m."bitspercomponent" = xobject.bitsPerComponent + m."colorspace" = xobject.colorSpace.name + m."suffix" = xobject.suffix + list << m + } + } + + @Override + void clip(int windingRule) throws IOException { + + } + + @Override + void moveTo(float x, float y) throws IOException { + + } + + @Override + void lineTo(float x, float y) throws IOException { + + } + + @Override + void curveTo(float x1, float y1, float x2, float y2, float x3, float y3) throws IOException { + + } + + @Override + Point2D getCurrentPoint() throws IOException { + null + } + + @Override + void closePath() throws IOException { + + } + + @Override + void endPath() throws IOException { + + } + + @Override + void strokePath() throws IOException { + + } + + @Override + void fillPath(int windingRule) throws IOException { + + } + + @Override + void fillAndStrokePath(int windingRule) throws IOException { + + } + + @Override + void shadingFill(COSName shadingName) throws IOException { + + } + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/DocumentBuilder.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/DocumentBuilder.groovy new file mode 100644 index 0000000..bb6529f --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/DocumentBuilder.groovy @@ -0,0 +1,166 @@ +package org.xbib.graphics.pdfbox.groovy.builder + +import org.xbib.graphics.pdfbox.groovy.BackgroundAssignable +import org.xbib.graphics.pdfbox.groovy.BaseNode +import org.xbib.graphics.pdfbox.groovy.BlockNode +import org.xbib.graphics.pdfbox.groovy.Bookmarkable +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Font +import org.xbib.graphics.pdfbox.groovy.Heading +import org.xbib.graphics.pdfbox.groovy.Linkable +import org.xbib.graphics.pdfbox.groovy.Stylable +import org.xbib.graphics.pdfbox.groovy.TextBlock +import org.xbib.graphics.pdfbox.groovy.UnitCategory +import org.xbib.graphics.pdfbox.groovy.factory.* + +/** + * + */ +abstract class DocumentBuilder extends FactoryBuilderSupport { + + final OutputStream out + + final List fontDefs + + RenderState renderState = RenderState.PAGE + + Document document + + protected List imageFileNames = [] + + DocumentBuilder(OutputStream out) { + this(out, null) + } + + DocumentBuilder(OutputStream out, List fontDefs) { + super(true) + this.out = out + this.fontDefs = fontDefs + } + + OutputStream getOutputStream() { + out + } + + Font getFont() { + current.font + } + + def invokeMethod(String name, args) { + use(UnitCategory) { + super.invokeMethod(name, args) + } + } + + void setNodeProperties(BaseNode node, Map attributes, String nodeKey) { + String[] templateKeys = getTemplateKeys(node, nodeKey) + def nodeProperties = [] + templateKeys.each { String key -> + if (document.template && document.templateMap.containsKey(key)) { + nodeProperties << document.templateMap[key] + } + } + nodeProperties << attributes + if (node instanceof Stylable) { + setNodeFont(node, nodeProperties) + } + if (node instanceof BlockNode) { + setBlockProperties(node, nodeProperties) + } + if (node instanceof BackgroundAssignable) { + setNodeBackground(node, nodeProperties) + } + if (node instanceof Linkable) { + String parentUrl = (node.parent instanceof Linkable) ? node.parent.url : null + node.url = node.url ?: parentUrl + } + if (node instanceof Bookmarkable) { + node.ref = attributes.ref + } + } + + protected void setNodeFont(Stylable node, nodeProperties) { + node.font = (node instanceof Document) ? new Font() : node.parent.font.clone() + nodeProperties.each { + node.font << it.font + } + } + + protected void setBlockProperties(BlockNode node, nodeProperties) { + node.margin = node.getClass().defaultMargin.clone() + nodeProperties.each { + node.margin << it.margin + if (it.border) { + node.border << it.border + } + } + } + + protected void setNodeBackground(BackgroundAssignable node, nodeProperties) { + nodeProperties.each { Map properties -> + if (properties.containsKey('background')) { + node.background = properties.background + } + } + if (!node.background && (node.parent instanceof BackgroundAssignable) && node.parent.background) { + node.background = "#${node.parent.background.hex}" + } + } + + static String[] getTemplateKeys(BaseNode node, String nodeKey) { + def keys = [nodeKey] + if (node instanceof Heading) { + keys << "heading${node.level}" + } + if (node instanceof Stylable && node.style) { + keys << "${nodeKey}.${node.style}" + if (node instanceof Heading) { + keys << "heading${node.level}.${node.style}" + } + } + keys + } + + TextBlock getColumnParagraph(Cell column) { + if (column.children && column.children[0] instanceof TextBlock) { + column.children[0] + } else { + TextBlock paragraph = new TextBlock(font: column.font.clone(), parent: column, align: column.align) + setNodeProperties(paragraph, [margin: [top: 0, left: 0, bottom: 0, right: 0]], 'paragraph') + column.children << paragraph + paragraph + } + } + + abstract void initializeDocument(Document document) + + abstract void writeDocument(Document document) + + def onTextBlockComplete + def onTableComplete + def onLineComplete + def onRowComplete + def onCellComplete + + def registerObjectFactories() { + registerFactory('create', new CreateFactory()) + registerFactory('document', new DocumentFactory()) + registerFactory('pageBreak', new PageBreakFactory()) + registerFactory('paragraph', new ParagraphFactory()) + registerFactory('lineBreak', new LineBreakFactory()) + registerFactory('line', new LineFactory()) + registerFactory('image', new ImageFactory()) + registerFactory('barcode', new BarcodeFactory()) + registerFactory('text', new TextFactory()) + registerFactory('table', new TableFactory()) + registerFactory('row', new RowFactory()) + registerFactory('cell', new CellFactory()) + registerFactory('heading1', new HeadingFactory()) + registerFactory('heading2', new HeadingFactory()) + registerFactory('heading3', new HeadingFactory()) + registerFactory('heading4', new HeadingFactory()) + registerFactory('heading5', new HeadingFactory()) + registerFactory('heading6', new HeadingFactory()) + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfDocument.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfDocument.groovy new file mode 100644 index 0000000..e45e233 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfDocument.groovy @@ -0,0 +1,138 @@ +package org.xbib.graphics.pdfbox.groovy.builder + +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.pdmodel.PDPage +import org.apache.pdfbox.pdmodel.PDPageContentStream +import org.apache.pdfbox.pdmodel.common.PDRectangle +import org.xbib.graphics.pdfbox.groovy.Document + +/** + * + */ +class PdfDocument implements Closeable { + + BigDecimal x = 0 + + BigDecimal y = 0 + + Document document + + PDDocument pdDocument + + Integer pageNumber = 0 + + PDPageContentStream contentStream + + List pages = [] + + PdfDocument(Document document) { + this.document = document + this.pdDocument = new PDDocument() + addPage() + } + + void toStartPosition() { + x = document.margin.left + y = document.margin.top + } + + int getPageBottomY() { + currentPage.mediaBox.height - document.margin.bottom + } + + void addPage() { + PDRectangle papersize + switch (document.papersize) { + case 'A0': + papersize = PDRectangle.A0 + break + case 'A1': + papersize = PDRectangle.A1 + break + case 'A2': + papersize = PDRectangle.A2 + break + case 'A3': + papersize = PDRectangle.A3 + break + case 'A4': + papersize = PDRectangle.A4 + break + case 'A5': + papersize = PDRectangle.A5 + break + case 'A6': + papersize = PDRectangle.A6 + break + case 'LETTER': + papersize = PDRectangle.LETTER + break + case 'LEGAL': + papersize = PDRectangle.LEGAL + break + default: + papersize = PDRectangle.A4 + break + } + switch (document.orientation) { + case 'landscape': + papersize = swapOrientation(papersize) + break; + case 'portrait': + default: + break + } + document.width = papersize.width as int + document.height = papersize.height as int + def newPage = new PDPage(papersize) + pages << newPage + pageNumber++ + contentStream?.close() + contentStream = new PDPageContentStream(pdDocument, currentPage) + toStartPosition() + pdDocument.addPage(newPage) + } + + static PDRectangle swapOrientation(PDRectangle pdRectangle) { + new PDRectangle(pdRectangle.height, pdRectangle.width) + } + + PDPage getCurrentPage() { + pages[pageNumber - 1] + } + + void setPageNumber(int value) { + this.pageNumber = value + contentStream?.close() + contentStream = new PDPageContentStream(pdDocument, currentPage, true, true) + toStartPosition() + } + + BigDecimal getTranslatedY() { + currentPage.mediaBox.height - y + } + + void scrollDownPage(BigDecimal amount) { + if (remainingPageHeight < amount) { + BigDecimal amountDiff = amount - remainingPageHeight + addPage() + y += amountDiff + } + else { + y += amount + } + } + + BigDecimal translateY(Number value) { + currentPage.mediaBox.height - value + } + + BigDecimal getRemainingPageHeight() { + (currentPage.mediaBox.height - document.margin.bottom) - y + } + + @Override + void close() { + pdDocument.close() + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfDocumentBuilder.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfDocumentBuilder.groovy new file mode 100644 index 0000000..03b348a --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfDocumentBuilder.groovy @@ -0,0 +1,220 @@ +package org.xbib.graphics.pdfbox.groovy.builder + +import groovy.transform.InheritConstructors +import groovy.util.logging.Log4j2 +import groovy.xml.MarkupBuilder +import org.apache.pdfbox.pdmodel.common.PDMetadata +import org.xbib.graphics.pdfbox.groovy.* +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Line +import org.xbib.graphics.pdfbox.groovy.render.ParagraphRenderer +import org.xbib.graphics.pdfbox.groovy.render.TableRenderer + +@Log4j2 +@InheritConstructors +class PdfDocumentBuilder extends DocumentBuilder { + + PdfDocument pdfDocument + + @Override + void initializeDocument(Document document) { + pdfDocument = new PdfDocument(document) + pdfDocument.x = document.margin.left + pdfDocument.y = document.margin.top + document.element = pdfDocument + if (fontDefs) { + fontDefs.each { fd -> + String s = fd.resource + s = s && s.startsWith('/') ? s : '/' + s + InputStream inputStream = getClass().getResourceAsStream(s) + if (inputStream) { + inputStream.withCloseable { + boolean loaded = PdfFont.addFont(pdfDocument.pdDocument, fd.name, inputStream, fd.bold, fd.italic) + if (!loaded) { + log.warn("font ${fd.name} not loaded") + } else { + log.info("font ${fd.name} added") + } + } + } else { + log.warn("font ${fd.name} not found in class path") + } + } + } + } + + @Override + void writeDocument(Document document) { + addHeaderFooter() + addMetadata() + pdfDocument.contentStream?.close() + pdfDocument.pdDocument.save(getOutputStream()) + pdfDocument.pdDocument.close() + } + + def addPageBreakToDocument = { PageBreak pageBreak, Document document -> + pdfDocument.addPage() + } + + def onTextBlockComplete = { TextBlock paragraph -> + if (renderState == RenderState.PAGE && paragraph.parent instanceof Document) { + int pageWidth = document.width - document.margin.left - document.margin.right + int maxLineWidth = pageWidth - paragraph.margin.left - paragraph.margin.right + int renderStartX = document.margin.left + paragraph.margin.left + int renderStartY = paragraph.margin.top + pdfDocument.x = renderStartX + pdfDocument.scrollDownPage(renderStartY) + ParagraphRenderer paragraphRenderer = new ParagraphRenderer(paragraph, pdfDocument, renderStartX, renderStartY, maxLineWidth) + while (!paragraphRenderer.fullyParsed) { + paragraphRenderer.parse(pdfDocument.remainingPageHeight) + paragraphRenderer.render(pdfDocument.x, pdfDocument.y) + if (paragraphRenderer.fullyParsed) { + pdfDocument.scrollDownPage(paragraphRenderer.renderedHeight) + } else { + pdfDocument.addPage() + } + } + pdfDocument.scrollDownPage(paragraph.margin.bottom) + } + } + + def onTableComplete = { Table table -> + if (renderState == RenderState.PAGE) { + pdfDocument.x = table.margin.left + document.margin.left + //pdfDocument.y = table.margin.top + document.margin.top // TODO + pdfDocument.scrollDownPage(table.margin.top) + TableRenderer tableRenderer = new TableRenderer(table, pdfDocument, pdfDocument.x, pdfDocument.y) + while (!tableRenderer.fullyParsed) { + tableRenderer.parse(pdfDocument.remainingPageHeight) + tableRenderer.render(pdfDocument.x, pdfDocument.y) + if (tableRenderer.fullyParsed) { + pdfDocument.scrollDownPage(tableRenderer.renderedHeight) + } else { + pdfDocument.addPage() + } + } + pdfDocument.scrollDownPage(table.margin.bottom) + } + } + + def onLineComplete = { Line line -> + /*if (renderState == RenderState.PAGE) { + LineRenderer lineRenderer = new LineRenderer(line, pdfDocument, pdfDocument.x, pdfDocument.y) + lineRenderer.render(pdfDocument.x, pdfDocument.y) + }*/ + } + + def onRowComplete = { Row row -> + } + + def onCellComplete = { Cell cell -> + } + + private void addHeaderFooter() { + int pageCount = pdfDocument.pages.size() + def options = new HeaderFooterOptions(pageCount: pageCount, dateGenerated: new Date()) + (1..pageCount).each { int pageNumber -> + pdfDocument.pageNumber = pageNumber + options.pageNumber = pageNumber + if (document.header) { + renderState = RenderState.HEADER + def header = document.header(options) + renderHeaderFooter(header) + } + if (document.footer) { + renderState = RenderState.FOOTER + def footer = document.footer(options) + renderHeaderFooter(footer) + } + } + renderState = RenderState.PAGE + } + + private void renderHeaderFooter(headerFooter) { + float startX = document.margin.left + headerFooter.margin.left + float startY + if (renderState == RenderState.HEADER) { + startY = headerFooter.margin.top + } else { + float pageBottom = pdfDocument.pageBottomY + document.margin.bottom + startY = pageBottom - getElementHeight(headerFooter) - headerFooter.margin.bottom + } + def renderer = null + if (headerFooter instanceof TextBlock) { + renderer = new ParagraphRenderer(headerFooter, pdfDocument, startX, startY, document.width) + } else if (headerFooter instanceof Table) { + renderer = new TableRenderer(headerFooter as Table, pdfDocument, startX, startY) + } + if (renderer) { + renderer.parse(document.height) + renderer.render(startX, startY) + } + } + + private float getElementHeight(element) { + float width = document.width - document.margin.top - document.margin.bottom + if (element instanceof TextBlock) { + new ParagraphRenderer(element, pdfDocument, 0, 0, width).totalHeight + } else if (element instanceof Table) { + new TableRenderer(element, pdfDocument, 0, 0).totalHeight + } else if (element instanceof Line) { + element.strokewidth + } else { + 0 + } + } + + private void addMetadata() { + ByteArrayOutputStream xmpOut = new ByteArrayOutputStream() + def xml = new MarkupBuilder(xmpOut.newWriter()) + xml.document(marginTop: "${document.margin.top}", marginBottom: "${document.margin.bottom}", + marginLeft: "${document.margin.left}", marginRight: "${document.margin.right}") { + delegate = xml + resolveStrategy = Closure.DELEGATE_FIRST + document.children.each { child -> + switch (child.getClass()) { + case TextBlock: + addParagraphToMetadata(delegate, child) + break + case Table: + addTableToMetadata(delegate, child) + break + } + } + } + def catalog = pdfDocument.pdDocument.documentCatalog + InputStream inputStream = new ByteArrayInputStream(xmpOut.toByteArray()) + PDMetadata metadata = new PDMetadata(pdfDocument.pdDocument, inputStream) + catalog.metadata = metadata + } + + private void addParagraphToMetadata(builder, TextBlock paragraphNode) { + builder.paragraph(marginTop: "${paragraphNode.margin.top}", + marginBottom: "${paragraphNode.margin.bottom}", + marginLeft: "${paragraphNode.margin.left}", + marginRight: "${paragraphNode.margin.right}") { + paragraphNode.children?.findAll { it.getClass() == Image }.each { + builder.image() + } + paragraphNode.children?.findAll { it.getClass() == Barcode }.each { + builder.barcode() + } + } + } + + private void addTableToMetadata(builder, Table tableNode) { + builder.table(columns: tableNode.columnCount, width: tableNode.width, borderSize: tableNode.border.size) { + delegate = builder + resolveStrategy = Closure.DELEGATE_FIRST + tableNode.children.each { + def cells = it.children + row { + cells.each { + cell(width: "${it.width ?: 0}") + } + } + } + } + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfFont.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfFont.groovy new file mode 100644 index 0000000..8fcd4b3 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfFont.groovy @@ -0,0 +1,81 @@ +package org.xbib.graphics.pdfbox.groovy.builder + +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.pdmodel.font.PDFont +import org.apache.pdfbox.pdmodel.font.PDType0Font +import org.apache.pdfbox.pdmodel.font.PDType1Font +import org.xbib.graphics.pdfbox.groovy.Font + +class PdfFont { + + private static final DEFAULT_FONT = PDType1Font.HELVETICA + + private static fonts = [ + 'Times-Roman': [regular: PDType1Font.TIMES_ROMAN, + bold: PDType1Font.TIMES_BOLD, + italic : PDType1Font.TIMES_ITALIC, + boldItalic: PDType1Font.TIMES_BOLD_ITALIC], + 'Helvetica' : [regular: PDType1Font.HELVETICA, + bold: PDType1Font.HELVETICA_BOLD, + italic : PDType1Font.HELVETICA_OBLIQUE, + boldItalic: PDType1Font.HELVETICA_BOLD_OBLIQUE], + 'Courier' : [regular: PDType1Font.COURIER, + bold: PDType1Font.COURIER_BOLD, + italic : PDType1Font.COURIER_OBLIQUE, + boldItalic: PDType1Font.COURIER_BOLD_OBLIQUE], + 'Symbol' : [regular: PDType1Font.SYMBOL], + 'Dingbat' : [regular: PDType1Font.ZAPF_DINGBATS] + ] + + static boolean addFont(PDDocument document, String name, InputStream inputStream, boolean bold, boolean italic) { + if (inputStream != null) { + PDType0Font font = PDType0Font.load(document, inputStream) + String fontName = name ?: font.baseFont + fonts[fontName] = fonts[fontName] ?: [:] + if (bold && italic) { + fonts[fontName].boldItalic = font + } else if (bold) { + fonts[fontName].bold = font + } else if (italic) { + fonts[fontName].italic = font + } else { + fonts[fontName].regular = font + } + fonts[fontName].regular = fonts[fontName].regular ?: font + true + } else { + false + } + } + + static PDFont getFont(Font font) { + if (!font?.family || !fonts.containsKey(font.family)) { + return DEFAULT_FONT + } + def fontOptions = fonts[font.family] + PDFont pdfFont = fontOptions.containsKey('regular') ? fontOptions.regular : DEFAULT_FONT + if (fontOptions) { + if (font.italic && font.bold) { + pdfFont = fontOptions.containsKey('boldItalic') ? fontOptions.boldItalic : pdfFont + } else if (font.italic) { + pdfFont = fontOptions.containsKey('italic') ? fontOptions.italic : pdfFont + } else if (font.bold) { + pdfFont = fontOptions.containsKey('bold') ? fontOptions.bold : pdfFont + } + } + pdfFont + } + + static boolean canEncode(Font font, String string) { + canEncode(getFont(font), string) + } + + static boolean canEncode(PDFont font, String string) { + try { + font.encode(string) + return true + } catch (Exception e) { + return false + } + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfboxRenderer.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfboxRenderer.groovy new file mode 100644 index 0000000..6f2c157 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/PdfboxRenderer.groovy @@ -0,0 +1,66 @@ +package org.xbib.graphics.pdfbox.groovy.builder + +import org.apache.pdfbox.pdmodel.PDPageContentStream +import org.xbib.graphics.barcode.HumanReadableLocation +import org.xbib.graphics.barcode.Symbol +import org.xbib.graphics.barcode.util.Hexagon +import org.xbib.graphics.barcode.util.TextBox + +import java.awt.geom.Rectangle2D + +class PdfboxRenderer { + + private final PDPageContentStream contentStream + + private final float startX + + private final float startY + + private final double scalingFactor + + PdfboxRenderer(PDPageContentStream contentStream, float startX, float startY, double scalingfactor) { + this.contentStream = contentStream + this.startX = startX + this.startY = startY + this.scalingFactor = scalingfactor + } + + void render(Symbol symbol) throws IOException { + Integer marginX = (symbol.getQuietZoneHorizontal() * scalingFactor) as Integer + Integer marginY = (symbol.getQuietZoneVertical() * scalingFactor) as Integer + // rectangles + for (Rectangle2D.Double rect : symbol.rectangles) { + float x = startX + (rect.x * scalingFactor) + marginX as float + float y = startY + (rect.y * scalingFactor) + marginY as float + float w = rect.width * scalingFactor as float + float h = rect.height * scalingFactor as float + contentStream.addRect(x, y, w, h) + contentStream.fill() + } + // human readable + if (symbol.getHumanReadableLocation() != HumanReadableLocation.NONE) { + for (TextBox text : symbol.texts) { + float x = startX + (text.x * scalingFactor) + marginX as float + float y = startY + (text.y * scalingFactor) + marginY as float + contentStream.beginText() + contentStream.moveTo(x, y) + contentStream.showText(text.text) + contentStream.endText() + } + } + // hexagon + for (Hexagon hexagon : symbol.hexagons) { + for (int j = 0; j < 6; j++) { + contentStream.moveTo(hexagon.pointX[j] as float, hexagon.pointY[j] as float) + contentStream.lineTo(hexagon.pointX[j] as float, hexagon.pointY[j] as float) + contentStream.closePath() + contentStream.fill() + } + } + // ellipsis + for (int i = 0; i < symbol.target.size(); i++) { + + // TODO + } + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/RenderState.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/RenderState.groovy new file mode 100644 index 0000000..325f886 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/builder/RenderState.groovy @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.groovy.builder + +enum RenderState { + PAGE, HEADER, FOOTER, CUSTOM +} \ No newline at end of file diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/BarcodeFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/BarcodeFactory.groovy new file mode 100644 index 0000000..a25ab24 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/BarcodeFactory.groovy @@ -0,0 +1,32 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Barcode +import org.xbib.graphics.pdfbox.groovy.TextBlock + +class BarcodeFactory extends AbstractFactory { + + @Override + Object newInstance(FactoryBuilderSupport builder, name, value, Map attributes) + throws InstantiationException, IllegalAccessException { + Barcode barcode = new Barcode(attributes) + TextBlock paragraph + if (builder.parentName == 'paragraph') { + paragraph = builder.current as TextBlock + } else { + paragraph = builder.getColumnParagraph(builder.current) + } + barcode.parent = paragraph + paragraph.children << barcode + barcode + } + + @Override + boolean isLeaf() { + true + } + + @Override + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { + false + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/CellFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/CellFactory.groovy new file mode 100644 index 0000000..bdfc0c6 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/CellFactory.groovy @@ -0,0 +1,37 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Row +import org.xbib.graphics.pdfbox.groovy.Text +import org.xbib.graphics.pdfbox.groovy.TextBlock + +class CellFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + Cell cell = new Cell(attributes) + Row row = builder.current as Row + cell.parent = row + builder.setNodeProperties(cell, attributes, 'cell') + if (value) { + TextBlock paragraph = builder.getColumnParagraph(cell) + List elements = paragraph.addText(value.toString()) + elements.each { node -> + if (node instanceof Text) { + builder.setNodeProperties(node, [:], 'text') + } + } + } + cell + } + + boolean isLeaf() { false } + + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { false } + + void onNodeCompleted(FactoryBuilderSupport builder, parent, child) { + if (builder.onCellComplete) { + builder.onCellComplete(child) + } + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/CreateFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/CreateFactory.groovy new file mode 100644 index 0000000..4622b08 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/CreateFactory.groovy @@ -0,0 +1,18 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +class CreateFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + [:] + } + + + boolean isLeaf() { + false + } + + void setChild(FactoryBuilderSupport builder, parent, child) { + parent.document = child + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/DocumentFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/DocumentFactory.groovy new file mode 100644 index 0000000..de7f7c2 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/DocumentFactory.groovy @@ -0,0 +1,27 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Document + +class DocumentFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + Document document = new Document(attributes) + builder.document = document + builder.setNodeProperties(document, attributes, 'document') + builder.initializeDocument(document) + document + } + + void setChild(FactoryBuilderSupport builder, parent, child) { + parent.children << child + } + + void onNodeCompleted(FactoryBuilderSupport builder, Object parent, Object node) { + builder.writeDocument(builder.document) + } + + boolean isLeaf() { false } + + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { false } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/HeadingFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/HeadingFactory.groovy new file mode 100644 index 0000000..26bb984 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/HeadingFactory.groovy @@ -0,0 +1,29 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Heading +import org.xbib.graphics.pdfbox.groovy.Text + +class HeadingFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + Heading heading = new Heading(attributes) + heading.level = Integer.valueOf(builder.currentName - 'heading') + heading.parent = builder.document + builder.setNodeProperties(heading, attributes, 'heading') + Text text = new Text(value: value, parent: heading) + heading.children << text + builder.setNodeProperties(text, [:], 'text') + heading + } + + void onNodeCompleted(FactoryBuilderSupport builder, parent, child) { + if (builder.onTextBlockComplete) { + builder.onTextBlockComplete(child) + } + } + + boolean isLeaf() { false } + + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { false } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/ImageFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/ImageFactory.groovy new file mode 100644 index 0000000..4945c9b --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/ImageFactory.groovy @@ -0,0 +1,110 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Image +import org.xbib.graphics.pdfbox.groovy.ImageType +import org.xbib.graphics.pdfbox.groovy.TextBlock + +import javax.imageio.ImageIO +import java.awt.image.BufferedImage +import java.security.MessageDigest + +class ImageFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + Image image = new Image(attributes) + if (!image.width || !image.height) { + InputStream inputStream = new ByteArrayInputStream(image.data) + // TODO add TwelveMonkeys + BufferedImage bufferedImage = ImageIO.read(inputStream) + image.width = bufferedImage.width + image.height = bufferedImage.height + bufferedImage.getType() + } + if (!image.name || builder.imageFileNames.contains(image.name)) { + image.name = generateImageName(image) + } + builder.imageFileNames << image.name + TextBlock paragraph + if (builder.parentName == 'paragraph') { + paragraph = builder.current as TextBlock + } else { + paragraph = builder.getColumnParagraph(builder.current) + } + image.parent = paragraph + paragraph.children << image + if (!image.type) { + String suffix = suffixOf(image.name) + switch (suffix) { + case 'bmp': + image.type = ImageType.BMP + break + case 'gif': + image.type = ImageType.GIF + break + case 'jpg': + case 'jpeg': + image.type = ImageType.JPG + break + case 'png': + image.type = ImageType.PNG + break + case 'tif': + case 'tiff': + image.type = ImageType.TIF + break + default: + image.type = ImageType.PNG + break + } + } + image + } + + String generateImageName(Image image) { + if (!image) { + throw new IllegalArgumentException('no image') + } + Formatter hexHash = new Formatter() + MessageDigest.getInstance('SHA-256').digest(image.data).each { b -> + hexHash.format('%02x', b) + } + String type = '' + if (image.type) { + switch (image.type) { + case ImageType.BMP: + type = 'bmp' + break + case ImageType.GIF: + type = 'gif' + break + case ImageType.JPG: + type = 'jpg' + break + case ImageType.PNG: + type = 'png' + break + case ImageType.TIF: + type = 'tif' + break + default: + type = 'png' + } + } + "${hexHash}.${type}" + } + + @Override + boolean isLeaf() { + true + } + + @Override + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { + false + } + + static String suffixOf(String filename) { + int pos = filename.lastIndexOf('.') + return pos > 0 ? filename.substring(pos + 1).toLowerCase(Locale.ROOT) : null + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/LineBreakFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/LineBreakFactory.groovy new file mode 100644 index 0000000..167e2fe --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/LineBreakFactory.groovy @@ -0,0 +1,22 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.LineBreak +import org.xbib.graphics.pdfbox.groovy.TextBlock + +class LineBreakFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + LineBreak lineBreak = new LineBreak() + TextBlock paragraph + if (builder.parentName == 'paragraph') { + paragraph = builder.current as TextBlock + } else { + paragraph = builder.getColumnParagraph(builder.current) + } + lineBreak.parent = paragraph + paragraph.children << lineBreak + lineBreak + } + + boolean isLeaf() { true } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/LineFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/LineFactory.groovy new file mode 100644 index 0000000..2bfa820 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/LineFactory.groovy @@ -0,0 +1,23 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Line + +class LineFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + Line line = new Line(attributes) + line.parent = builder.parentName == 'create' ? builder.document : builder.current + if (builder.parentName == 'paragraph' || builder.parentName == 'cell') { + line.parent.children << line + } + line + } + + void onNodeCompleted(FactoryBuilderSupport builder, parent, line) { + if (builder.onLineComplete) { + builder.onLineComplete(line) + } + } + + boolean isLeaf() { true } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/PageBreakFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/PageBreakFactory.groovy new file mode 100644 index 0000000..fac4517 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/PageBreakFactory.groovy @@ -0,0 +1,17 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.PageBreak + +class PageBreakFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + PageBreak pageBreak = new PageBreak() + pageBreak.parent = builder.document + if (builder.addPageBreakToDocument) { + builder.addPageBreakToDocument(pageBreak, builder.document) + } + pageBreak + } + + boolean isLeaf() { true } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/ParagraphFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/ParagraphFactory.groovy new file mode 100644 index 0000000..e8f75e4 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/ParagraphFactory.groovy @@ -0,0 +1,38 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Align +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Text +import org.xbib.graphics.pdfbox.groovy.TextBlock + +class ParagraphFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + TextBlock paragraph = new TextBlock(attributes) + paragraph.parent = builder.parentName == 'create' ? builder.document : builder.current + builder.setNodeProperties(paragraph, attributes, 'paragraph') + if (paragraph.parent instanceof Document) { + paragraph.align = paragraph.align ?: Align.LEFT + } + if (value) { + List elements = paragraph.addText(value.toString()) + elements.each { node -> + if (node instanceof Text) { + builder.setNodeProperties(node, [:], 'text') + } + } + } + paragraph + } + + boolean isLeaf() { false } + + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { false } + + void onNodeCompleted(FactoryBuilderSupport builder, parent, child) { + if (builder.onTextBlockComplete) { + builder.onTextBlockComplete(child) + } + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/RowFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/RowFactory.groovy new file mode 100644 index 0000000..1178d37 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/RowFactory.groovy @@ -0,0 +1,29 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Row + +class RowFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + Row row = new Row(attributes) + row.parent = builder.current + builder.setNodeProperties(row, attributes, 'row') + row + } + + void setChild(FactoryBuilderSupport builder, row, column) { + column.parent = row + row.children << column + } + + boolean isLeaf() { false } + + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { false } + + void onNodeCompleted(FactoryBuilderSupport builder, parent, child) { + if (builder.onRowComplete) { + builder.onRowComplete(child) + } + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/TableFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/TableFactory.groovy new file mode 100644 index 0000000..3de789b --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/TableFactory.groovy @@ -0,0 +1,37 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.builder.RenderState + +class TableFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + Table table = new Table(attributes) + table.parent = builder.parentName == 'create' ? builder.document : builder.current + if (table.parent instanceof Cell) { + table.parent.children << table + } + builder.setNodeProperties(table, attributes, 'table') + table + } + + void setChild(FactoryBuilderSupport builder, table, row) { + table.children << row + } + + void onNodeCompleted(FactoryBuilderSupport builder, parent, table) { + if (parent instanceof Document || builder.renderState != RenderState.PAGE) { + table.normalizeColumnWidths() + } + if (builder.onTableComplete) { + builder.onTableComplete(table) + } + } + + boolean isLeaf() { false } + + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { false } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/TextFactory.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/TextFactory.groovy new file mode 100644 index 0000000..6a2a4b9 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/factory/TextFactory.groovy @@ -0,0 +1,30 @@ +package org.xbib.graphics.pdfbox.groovy.factory + +import org.xbib.graphics.pdfbox.groovy.Text +import org.xbib.graphics.pdfbox.groovy.TextBlock + +class TextFactory extends AbstractFactory { + + def newInstance(FactoryBuilderSupport builder, name, value, Map attributes) { + TextBlock paragraph + if (builder.parentName == 'paragraph') { + paragraph = builder.current as TextBlock + } else { + paragraph = builder.getColumnParagraph(builder.current) + } + List elements = paragraph.addText(value.toString()) + elements.each { node -> + node.parent = paragraph + if (node instanceof Text) { + node.url = attributes.url + node.style = attributes.style + builder.setNodeProperties(node, attributes, 'text') + } + } + elements + } + + boolean isLeaf() { true } + + boolean onHandleNodeAttributes(FactoryBuilderSupport builder, node, Map attributes) { false } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/CellRenderer.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/CellRenderer.groovy new file mode 100644 index 0000000..0bd2409 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/CellRenderer.groovy @@ -0,0 +1,110 @@ +package org.xbib.graphics.pdfbox.groovy.render + +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Line +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.TextBlock +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument + +class CellRenderer implements Renderable { + + BigDecimal currentRowHeight = 0 + + BigDecimal renderedHeight = 0 + + Cell cell + + List childRenderers = [] + + CellRenderer(Cell cell, PdfDocument pdfDocument, BigDecimal startX, BigDecimal startY) { + this.cell = cell + this.startX = startX + this.startY = startY + this.pdfDocument = pdfDocument + Table table = cell.parent.parent + BigDecimal renderWidth = cell.width - (table.padding + table.padding) + BigDecimal childStartX = startX + table.padding + BigDecimal childStartY = startY + table.padding + cell.children.each { child -> + if (child instanceof TextBlock) { + childRenderers << new ParagraphRenderer(child, pdfDocument, childStartX, childStartY, renderWidth) + } else if (child instanceof Line) { + childRenderers << new LineRenderer(child, pdfDocument, childStartX, childStartY) + } else if (child instanceof Table) { + childRenderers << new TableRenderer(child, pdfDocument, childStartX, childStartY) + } + } + } + + BigDecimal getRowspanHeight() { + cell.rowspanHeight + currentRowHeight + } + + BigDecimal getPadding() { + cell.parent.parent.padding + } + + @Override + Boolean getFullyParsed() { + if (cell.rowspan > 1 && !onLastRowspanRow) { + return true + } + childRenderers.every { it.fullyParsed } + } + + @Override + BigDecimal getTotalHeight() { + (childRenderers*.totalHeight.sum() as BigDecimal ?: 0) + (padding * 2) + } + + @Override + BigDecimal getParsedHeight() { + if (!childRenderers || !onLastRowspanRow) { + return 0 + } + BigDecimal parsedHeight = (childRenderers*.parsedHeight.sum() as BigDecimal) ?: 0 + if (onFirstPage && parsedHeight) { + parsedHeight += padding + } + if (fullyParsed) { + parsedHeight += padding + } + if (cell.rowspan > 1) { + parsedHeight -= cell.rowspanHeight + } + parsedHeight + } + + @Override + void renderElement(BigDecimal startX, BigDecimal startY) { + BigDecimal childX = startX + BigDecimal childY = startY + if (cell.rowspan > 1) { + childY -= cell.rowspanHeight + } + if (onFirstPage) { + childY += padding + } + if (onLastRowspanRow) { + childRenderers*.render(childX, childY) + } + else { + cell.rowspanHeight += currentRowHeight + currentRowHeight = 0 + } + renderedHeight = parsedHeight + } + + @Override + void parse(BigDecimal height) { + if (height < 0) { + return + } + childRenderers*.parse(height - padding) + } + + Boolean isOnLastRowspanRow() { + (cell.rowspan == 1) || (cell.rowsSpanned == (cell.rowspan - 1)) + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/LineRenderer.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/LineRenderer.groovy new file mode 100644 index 0000000..bda3b4f --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/LineRenderer.groovy @@ -0,0 +1,57 @@ +package org.xbib.graphics.pdfbox.groovy.render + +import org.apache.pdfbox.pdmodel.PDPageContentStream +import org.xbib.graphics.pdfbox.groovy.Line +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument + +class LineRenderer implements Renderable { + + private Line line + + LineRenderer(Line line, PdfDocument pdfDocument, BigDecimal startX, BigDecimal startY) { + this.line = line + this.pdfDocument = pdfDocument + this.startX = startX + this.startY = startY + } + + @Override + void parse(BigDecimal maxHeight) { + } + + @Override + Boolean getFullyParsed() { + true + } + + @Override + BigDecimal getTotalHeight() { + line.strokewidth + } + + @Override + BigDecimal getParsedHeight() { + line.strokewidth + } + + @Override + BigDecimal getRenderedHeight() { + line.strokewidth + } + + @Override + void renderElement(BigDecimal startX, BigDecimal startY) { + if (parsedHeight == 0d) { + return + } + PDPageContentStream contentStream = pdfDocument.contentStream + contentStream.setLineWidth(line.strokewidth) + BigDecimal x1 = startX + line.startX + BigDecimal y1 = startY + line.startY + contentStream.moveTo(x1 as float, pdfDocument.translateY(y1) as float) + BigDecimal x2 = startX + line.endX + BigDecimal y2 = startY + line.endY + contentStream.lineTo(x2 as float, pdfDocument.translateY(y2) as float) + contentStream.stroke() + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphLine.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphLine.groovy new file mode 100644 index 0000000..63bb3e5 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphLine.groovy @@ -0,0 +1,43 @@ +package org.xbib.graphics.pdfbox.groovy.render + +import groovy.util.logging.Log4j2 +import org.xbib.graphics.pdfbox.groovy.TextBlock +import org.xbib.graphics.pdfbox.groovy.render.element.ImageElement +import org.xbib.graphics.pdfbox.groovy.render.element.TextElement + +@Log4j2 +class ParagraphLine { + + final BigDecimal maxWidth + + BigDecimal contentWidth = 0 + + TextBlock paragraph + + List elements = [] + + ParagraphLine(TextBlock paragraph, BigDecimal maxWidth) { + this.paragraph = paragraph + this.maxWidth = maxWidth + } + + BigDecimal getRemainingWidth() { + maxWidth - contentWidth + } + + BigDecimal getTotalHeight() { + getContentHeight() + } + + BigDecimal getContentHeight() { + elements.collect { + if (it instanceof TextElement) { + it.node.font.size * it.heightfactor + } else if (it instanceof ImageElement) { + it.node.height + } else { + 0 + } + }.max() as BigDecimal ?: paragraph.font.size + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphParser.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphParser.groovy new file mode 100644 index 0000000..ae1faf0 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphParser.groovy @@ -0,0 +1,136 @@ +package org.xbib.graphics.pdfbox.groovy.render + +import groovy.util.logging.Log4j2 +import org.apache.pdfbox.pdmodel.font.PDFont +import org.xbib.graphics.pdfbox.groovy.Barcode +import org.xbib.graphics.pdfbox.groovy.Font +import org.xbib.graphics.pdfbox.groovy.Image +import org.xbib.graphics.pdfbox.groovy.Line +import org.xbib.graphics.pdfbox.groovy.LineBreak +import org.xbib.graphics.pdfbox.groovy.Text +import org.xbib.graphics.pdfbox.groovy.TextBlock +import org.xbib.graphics.pdfbox.groovy.builder.PdfFont +import org.xbib.graphics.pdfbox.groovy.render.element.BarcodeElement +import org.xbib.graphics.pdfbox.groovy.render.element.ImageElement +import org.xbib.graphics.pdfbox.groovy.render.element.LineElement +import org.xbib.graphics.pdfbox.groovy.render.element.TextElement + +@Log4j2 +class ParagraphParser { + + static List getLines(TextBlock paragraph, BigDecimal maxLineWidth) { + + def lines = [] + + def currentChunk = [] + + def paragraphChunks = [] + + paragraphChunks << currentChunk + + paragraph.children.each { child -> + if (child.getClass() == LineBreak) { + currentChunk = [] + paragraphChunks << currentChunk + } else { + currentChunk << child + } + } + + paragraphChunks.each { lines += parseParagraphChunk(it, paragraph, maxLineWidth) } + lines + } + + private static List parseParagraphChunk(chunk, TextBlock paragraph, BigDecimal maxLineWidth) { + def chunkLines = [] + ParagraphLine currentLine = new ParagraphLine(paragraph, maxLineWidth) + chunkLines << currentLine + PDFont pdfFont + chunk.each { node -> + if (node.class == Text) { + Font font = node.font + pdfFont = PdfFont.getFont(font) + String remainingText = node.value + while (remainingText) { + BigDecimal heightFactor = paragraph.properties.heightfactor ?: 1.5 + BigDecimal size = font.size as BigDecimal + BigDecimal textWidth = getTextWidth(remainingText, pdfFont, size) + if (currentLine.contentWidth + textWidth > maxLineWidth) { + String text = getTextUntilBreak(remainingText, pdfFont, size, currentLine.remainingWidth) + int nextPosition = text.size() + remainingText = remainingText[nextPosition..-1].trim() + int elementWidth = getTextWidth(text, pdfFont, size) as int + currentLine.contentWidth += elementWidth + currentLine.elements << new TextElement(pdfFont: pdfFont, text: text, node: node, + width: elementWidth, heightfactor: heightFactor) + currentLine = new ParagraphLine(paragraph, maxLineWidth) + chunkLines << currentLine + } else { + currentLine.elements << new TextElement(pdfFont: pdfFont, text: remainingText, node: node, + width: textWidth, heightfactor: heightFactor) + remainingText = '' + currentLine.contentWidth += textWidth + } + + } + } else if (node.class == Line) { + currentLine.elements << new LineElement(node: node) + } else if (node.class == Image) { + if (currentLine.remainingWidth < node.width) { + currentLine = new ParagraphLine(paragraph, maxLineWidth) + chunkLines << currentLine + } + currentLine.contentWidth += node.width + currentLine.elements << new ImageElement(node: node) + } else if (node.class == Barcode) { + if (currentLine.remainingWidth < node.width) { + currentLine = new ParagraphLine(paragraph, maxLineWidth) + chunkLines << currentLine + } + currentLine.contentWidth += node.width + currentLine.elements << new BarcodeElement(node: node) + } else { + throw new IllegalStateException('unknown element class ' + node.class.name) + } + } + chunkLines + } + + private static String getTextUntilBreak(String text, PDFont font, BigDecimal fontSize, BigDecimal width) { + String result = '' + String previousResult = '' + boolean spaceBreakpointFound = false + String[] words = text.split()*.trim() + int wordIndex = 0 + BigDecimal resultWidth = 0 + while (words && resultWidth < width && wordIndex < words.size()) { + result += (wordIndex == 0 ? '' : ' ') + words[wordIndex] + resultWidth = getTextWidth(result, font, fontSize) + if (resultWidth == width) { + spaceBreakpointFound = true + break + } else if (resultWidth < width) { + spaceBreakpointFound = true + } else if (resultWidth > width) { + result = previousResult + break + } + wordIndex++ + previousResult = result + } + if (!spaceBreakpointFound) { + int currentCharacter = 0 + while (getTextWidth(result, font, fontSize) < width) { + result += text[currentCharacter] + currentCharacter++ + } + result = result.length() > 0 ? result.subSequence(0, result.length() - 1) : '' + } + result + } + + private static BigDecimal getTextWidth(String text, PDFont font, BigDecimal fontSize) { + // getStringWidth: not a cheap operation, and full of run time exceptions! + font.getStringWidth(text.replaceAll("\\p{C}", "")) / 1000 * fontSize + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphRenderer.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphRenderer.groovy new file mode 100644 index 0000000..abfd40a --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/ParagraphRenderer.groovy @@ -0,0 +1,244 @@ +package org.xbib.graphics.pdfbox.groovy.render + +import groovy.util.logging.Log4j2 +import org.apache.pdfbox.pdmodel.PDDocument +import org.apache.pdfbox.pdmodel.PDPageContentStream +import org.apache.pdfbox.pdmodel.PDResources +import org.apache.pdfbox.pdmodel.common.PDRectangle +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject +import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream +import org.xbib.graphics.barcode.Code3Of9 +import org.xbib.graphics.barcode.HumanReadableLocation +import org.xbib.graphics.barcode.render.GraphicsRenderer +import org.xbib.graphics.pdfbox.PdfBoxGraphics2D +import org.xbib.graphics.pdfbox.groovy.* +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument +import org.xbib.graphics.pdfbox.groovy.render.element.BarcodeElement +import org.xbib.graphics.pdfbox.groovy.render.element.ImageElement +import org.xbib.graphics.pdfbox.groovy.render.element.LineElement +import org.xbib.graphics.pdfbox.groovy.render.element.TextElement + +import javax.imageio.ImageIO +import java.awt.Rectangle +import java.awt.image.BufferedImage + +@Log4j2 +class ParagraphRenderer implements Renderable { + + TextBlock node + + List lines + + BigDecimal renderedHeight = 0 + + private Integer parseStart = 0 + + private Integer linesParsed = 0 + + private BigDecimal startX + + private BigDecimal startY + + private Boolean parsedAndRendered = false + + private Boolean fullyRendered = false + + private Boolean fullyParsed = false + + ParagraphRenderer(TextBlock paragraph, PdfDocument pdfDocument, BigDecimal startX, BigDecimal startY, BigDecimal maxWidth) { + this.node = paragraph + this.pdfDocument = pdfDocument + this.startX = startX + this.startY = startY + lines = ParagraphParser.getLines(paragraph, maxWidth) + } + + Boolean getFullyParsed() { + this.fullyParsed + } + + int getParseStart() { + this.parseStart + } + + int getParseEnd() { + int parseEnd = Math.max(0, parseStart + linesParsed - 1) as int + Math.min(lines.size() - 1, parseEnd) + } + + int getLinesParsed() { + this.linesParsed + } + + @Override + void parse(BigDecimal height) { + if (!lines || fullyRendered) { + fullyParsed = true + return + } + if (parsedAndRendered) { + parseStart += linesParsed + parseStart = Math.min(lines.size() - 1, parseStart) + } + linesParsed = 0 + boolean reachedEnd = false + BigDecimal parsedHeight = 0 + while (!reachedEnd) { + ParagraphLine line = lines[parseStart + linesParsed] + if (line.getTotalHeight() > 0) { + parsedHeight += line.getTotalHeight() + linesParsed++ + if (parsedHeight > height) { + linesParsed = Math.max(0f, linesParsed - 1) as int + reachedEnd = true + fullyParsed = false + } else if (line == lines.last()) { + reachedEnd = true + fullyParsed = true + } + } else { + linesParsed++ + if (line == lines.last()) { + reachedEnd = true + fullyParsed = true + } + } + } + parsedAndRendered = false + } + + @Override + void renderElement(BigDecimal startX, BigDecimal startY) { + if (fullyRendered || !linesParsed) { + return + } + pdfDocument.x = startX + pdfDocument.y = startY + lines[parseStart..parseEnd].each { ParagraphLine line -> + renderLine(line) + } + renderedHeight = getParsedHeight() + fullyRendered = fullyParsed + parsedAndRendered = true + } + + @Override + BigDecimal getTotalHeight() { + lines.sum { it.totalHeight } as BigDecimal + } + + @Override + BigDecimal getParsedHeight() { + if (!linesParsed) { + return 0 + } + lines[parseStart..parseEnd]*.totalHeight.sum() as BigDecimal ?: 0 + } + + private void renderLine(ParagraphLine line) { + BigDecimal renderStartX = startX + BigDecimal delta = line.maxWidth - line.contentWidth + switch (line.paragraph.align) { + case Align.RIGHT: + renderStartX += delta + break + case Align.CENTER: + renderStartX += (delta / 2) + break + } + pdfDocument.x = renderStartX + pdfDocument.y += line.getContentHeight() + line.elements.each { element -> + switch (element.getClass()) { + case TextElement: + renderTextElement(element as TextElement) + pdfDocument.x += element.width + break + case LineElement: + renderLineElement(element as LineElement) + break + case ImageElement: + renderImageElement(element as ImageElement) + break + case BarcodeElement: + renderBarcodeElement(element as BarcodeElement) + } + } + } + + private void renderTextElement(TextElement element) { + Text text = element.node + PDPageContentStream contentStream = pdfDocument.contentStream + contentStream.beginText() + contentStream.newLineAtOffset(pdfDocument.x as float, pdfDocument.translatedY as float) + def color = text.font.color.rgb + contentStream.setNonStrokingColor(color[0], color[1], color[2]) + contentStream.setFont(element.pdfFont, text.font.size) + String string = element.text.replaceAll("\\p{C}","") + contentStream.showText(string) // remove control chars + contentStream.endText() + } + + private void renderLineElement(LineElement element) { + Line line = element.node + PDPageContentStream contentStream = pdfDocument.contentStream + BigDecimal x1 = pdfDocument.x + line.startX + BigDecimal y1 = pdfDocument.translateY(pdfDocument.y + line.startY) + contentStream.moveTo(x1 as float, y1 as float) + BigDecimal x2 = pdfDocument.x + line.endX + BigDecimal y2 = pdfDocument.translateY(pdfDocument.y + line.endY) + contentStream.lineTo(x2 as float, y2 as float) + contentStream.setLineWidth(line.strokewidth) + contentStream.stroke() + } + + private void renderImageElement(ImageElement element) { + Image image = element.node + ImageType imageType = element.node.type + if (imageType) { + InputStream inputStream = new ByteArrayInputStream(element.node.data) + // TODO add TwelveMonkeys + BufferedImage bufferedImage = ImageIO.read(inputStream) + BigDecimal x = pdfDocument.x + image.x + BigDecimal y = pdfDocument.translateY(pdfDocument.y + image.y) + int width = element.node.width + int height = element.node.height + PDImageXObject img = imageType == ImageType.JPG ? + JPEGFactory.createFromImage(pdfDocument.pdDocument, bufferedImage) : + LosslessFactory.createFromImage(pdfDocument.pdDocument, bufferedImage) + pdfDocument.contentStream.drawImage(img, x as float, y as float, width, height) + inputStream.close() + } + } + + private void renderBarcodeElement(BarcodeElement element) { + switch (element.node.getType()) { + case BarcodeType.CODE39: + float x = pdfDocument.x + element.node.x + float y = pdfDocument.translateY(pdfDocument.y + element.node.y) + Code3Of9 code3Of9 = new Code3Of9() + code3Of9.setContent(element.node.value) + code3Of9.setBarHeight(element.node.height) + code3Of9.setHumanReadableLocation(HumanReadableLocation.NONE) + PDFormXObject formXObject = createXObject(pdfDocument.pdDocument, x, y) + PdfBoxGraphics2D g2d = new PdfBoxGraphics2D(pdfDocument.pdDocument, formXObject, pdfDocument.contentStream, null) + Rectangle rectangle = new Rectangle(0, 0, x as int, y as int) + GraphicsRenderer graphicsRenderer = + new GraphicsRenderer(g2d, rectangle, 1.0d, java.awt.Color.WHITE, java.awt.Color.BLACK, false, false); + graphicsRenderer.render(code3Of9); + graphicsRenderer.close(); + break + } + } + + private static PDFormXObject createXObject(PDDocument document, float x, float y) { + PDFormXObject xFormObject = new PDAppearanceStream(document) + xFormObject.setResources(new PDResources()) + xFormObject.setBBox(new PDRectangle(x, y)) + return xFormObject + } + +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/Renderable.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/Renderable.groovy new file mode 100644 index 0000000..3401cfd --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/Renderable.groovy @@ -0,0 +1,41 @@ +package org.xbib.graphics.pdfbox.groovy.render + +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument + +trait Renderable { + + BigDecimal startX + + BigDecimal startY + + PdfDocument pdfDocument + + abstract void parse(BigDecimal maxHeight) + + abstract Boolean getFullyParsed() + + abstract BigDecimal getTotalHeight() + + abstract BigDecimal getParsedHeight() + + abstract BigDecimal getRenderedHeight() + + abstract void renderElement(BigDecimal startX, BigDecimal startY) + + int renderCount = 0 + + void render(BigDecimal startX, BigDecimal startY) { + BigDecimal currentX = pdfDocument.x + BigDecimal currentY = pdfDocument.y + pdfDocument.y = startY + pdfDocument.x = startX + renderElement(startX, startY) + pdfDocument.x = currentX + pdfDocument.y = currentY + renderCount = renderCount + 1 + } + + Boolean getOnFirstPage() { + renderCount <= 1 + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/RowRenderer.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/RowRenderer.groovy new file mode 100644 index 0000000..953ae02 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/RowRenderer.groovy @@ -0,0 +1,145 @@ +package org.xbib.graphics.pdfbox.groovy.render + +import org.apache.pdfbox.pdmodel.PDPageContentStream +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Row +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument + +class RowRenderer implements Renderable { + + Row row + + List cellRenderers = [] + + BigDecimal renderedHeight = 0 + + RowRenderer(Row row, PdfDocument pdfDocument, BigDecimal startX, BigDecimal startY) { + this.row = row + this.startX = startX + this.startY = startY + this.pdfDocument = pdfDocument + Table table = row.parent + BigDecimal columnX = startX + table.border.size + BigDecimal columnY = startY + table.border.size + row.children.each { Cell column -> + cellRenderers << new CellRenderer(column, pdfDocument, columnX, columnY) + columnX += column.width + table.border.size + } + } + + @Override + void parse(BigDecimal height) { + cellRenderers*.parse(height) + cellRenderers*.currentRowHeight = parsedHeight + } + + @Override + Boolean getFullyParsed() { + cellRenderers.every { it.fullyParsed } + } + + @Override + BigDecimal getTotalHeight() { + cellRenderers*.totalHeight.max() + table.border.size + } + + @Override + BigDecimal getParsedHeight() { + BigDecimal parsedHeight = cellRenderers*.parsedHeight.max() as BigDecimal ?: 0 + if (fullyParsed && parsedHeight > 0) { + parsedHeight += table.border.size + } + parsedHeight + } + + @Override + void renderElement(BigDecimal startX, BigDecimal startY) { + if (parsedHeight == 0) { + return + } + renderBackgrounds(startX, startY) + renderBorders(startX, startY) + cellRenderers*.render(startX, startY) + renderedHeight = parsedHeight + } + + private Table getTable() { + row.parent + } + + BigDecimal getTableBorderOffset() { + table.border.size / 2 + } + + private void renderBackgrounds(BigDecimal startX, BigDecimal startY) { + BigDecimal backgroundStartY = startY + parsedHeight + if (!firstRow) { + backgroundStartY += tableBorderOffset + } + if (!fullyParsed) { + backgroundStartY -= table.border.size + } + BigDecimal translatedStartY = pdfDocument.translateY(backgroundStartY) + PDPageContentStream contentStream = pdfDocument.contentStream + cellRenderers.each { CellRenderer columnElement -> + Cell column = columnElement.cell + if (column.background) { + Boolean isLastColumn = (column == column.parent.children.last()) + contentStream.setNonStrokingColor(*column.background.rgb) + startX = columnElement.startX - tableBorderOffset + BigDecimal width = column.width + (isLastColumn ? table.border.size : tableBorderOffset) + BigDecimal height = parsedHeight - (fullyParsed ? 0 : tableBorderOffset) + height += ((fullyParsed && !onFirstPage) ? table.border.size : 0) + contentStream.addRect(startX as float, translatedStartY as float, + width as float, height as float) + contentStream.fill() + } + } + } + + private void renderBorders(BigDecimal startX, BigDecimal startY) { + if (!table.border.size) { + return + } + BigDecimal translatedYTop = pdfDocument.translateY(startY - tableBorderOffset) + BigDecimal translatedYBottom = pdfDocument.translateY(startY + parsedHeight) + BigDecimal rowStartX = startX - tableBorderOffset + BigDecimal rowEndX = startX + table.width + PDPageContentStream contentStream = pdfDocument.contentStream + def borderColor = table.border.color.rgb + contentStream.setStrokingColor(*borderColor) + contentStream.setLineWidth(table.border.size) + if (firstRow || isTopOfPage(startY)) { + contentStream.moveTo(rowStartX as float, translatedYTop as float) + contentStream.lineTo(rowEndX as float, translatedYTop as float) + contentStream.stroke() + } + cellRenderers.eachWithIndex { columnElement, i -> + if (i == 0) { + BigDecimal firstLineStartX = columnElement.startX - table.border.size + contentStream.moveTo(firstLineStartX as float, translatedYTop as float) + contentStream.lineTo(firstLineStartX as float, translatedYBottom as float) + contentStream.stroke() + } + BigDecimal columnStartX = columnElement.startX - table.border.size + BigDecimal columnEndX = columnElement.startX + columnElement.cell.width + tableBorderOffset + contentStream.moveTo(columnEndX as float, translatedYTop as float) + contentStream.lineTo(columnEndX as float, translatedYBottom as float) + contentStream.stroke() + if (fullyParsed && columnElement.onLastRowspanRow) { + contentStream.moveTo(columnStartX as float, translatedYBottom as float) + contentStream.lineTo(columnEndX as float, translatedYBottom as float) + contentStream.stroke() + } + } + } + + Boolean isTopOfPage(BigDecimal y) { + (y as int) == pdfDocument.document.margin.top + } + + Boolean isFirstRow() { + row == row.parent.children.first() + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/TableRenderer.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/TableRenderer.groovy new file mode 100644 index 0000000..9fe959d --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/TableRenderer.groovy @@ -0,0 +1,112 @@ +package org.xbib.graphics.pdfbox.groovy.render + +import org.xbib.graphics.pdfbox.groovy.Row +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument + +class TableRenderer implements Renderable { + + Table table + + List rowRenderers = [] + + BigDecimal renderedHeight = 0 + + int parseStart = 0 + + int parseEnd = 0 + + private boolean parsedAndRendered = false + + TableRenderer(Table table, PdfDocument pdfDocument, BigDecimal startX, BigDecimal startY) { + this.startX = startX + this.startY = startY + this.pdfDocument = pdfDocument + this.table = table + table.children.each { Row row -> + rowRenderers << new RowRenderer(row, pdfDocument, startX, startY) + } + } + + @Override + void parse(BigDecimal height) { + if (!rowRenderers) { + return + } + if (!parsedAndRendered) { + parseEnd = parseStart + } + boolean reachedEnd = false + BigDecimal remainingHeight = height - (onFirstPage ? table.border.size : 0) + while (!reachedEnd) { + RowRenderer currentRenderer = rowRenderers[parseEnd] + currentRenderer.parse(remainingHeight) + remainingHeight -= currentRenderer.parsedHeight + if (currentRenderer.parsedHeight == 0) { + reachedEnd = true + } + if (remainingHeight < 0) { + currentRenderer.parse(0 as BigDecimal) + parseEnd = Math.max(0, parseEnd - 1) + reachedEnd = true + } else if (remainingHeight == 0) { + reachedEnd = true + } else if (currentRenderer == rowRenderers.last()) { + reachedEnd = true + } + if (!reachedEnd && currentRenderer.fullyParsed) { + parseEnd++ + } + } + if (parseEnd >= rowRenderers.size()) { + parseEnd = rowRenderers.size() - 1 + } + if (parseEnd < parseStart) { + parseEnd = parseStart + } + parsedAndRendered = false + } + + @Override + Boolean getFullyParsed() { + rowRenderers ? rowRenderers.every { it.fullyParsed } : true + } + + @Override + BigDecimal getTotalHeight() { + (rowRenderers*.totalHeight.sum() ?: 0) + table.border.size + } + + @Override + BigDecimal getParsedHeight() { + (rowRenderers[parseStart..parseEnd]*.parsedHeight.sum() as BigDecimal?: 0) + + (onFirstPage ? table.border.size : 0) + } + + @Override + void renderElement(BigDecimal startX, BigDecimal startY) { + if (parsedAndRendered) { + return + } + BigDecimal rowStartX = startX + BigDecimal rowStartY = startY + Boolean lastRowRendered = false + rowRenderers[parseStart..parseEnd].each { + it.render(rowStartX, rowStartY) + rowStartY += it.parsedHeight + lastRowRendered = it.fullyParsed + if (lastRowRendered) { + it.cellRenderers.each { it.cell.rowsSpanned++ } + } + } + renderedHeight = parsedHeight + if (lastRowRendered) { + parseStart = Math.min(rowRenderers.size() - 1, parseEnd + 1) + parseEnd = parseStart + } + else { + parseStart = parseEnd + } + parsedAndRendered = true + } +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/BarcodeElement.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/BarcodeElement.groovy new file mode 100644 index 0000000..152de4d --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/BarcodeElement.groovy @@ -0,0 +1,8 @@ +package org.xbib.graphics.pdfbox.groovy.render.element + +import org.xbib.graphics.pdfbox.groovy.Barcode + +class BarcodeElement { + + Barcode node +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/ImageElement.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/ImageElement.groovy new file mode 100644 index 0000000..41494fe --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/ImageElement.groovy @@ -0,0 +1,8 @@ +package org.xbib.graphics.pdfbox.groovy.render.element + +import org.xbib.graphics.pdfbox.groovy.Image + +class ImageElement { + + Image node +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/LineElement.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/LineElement.groovy new file mode 100644 index 0000000..8955c5b --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/LineElement.groovy @@ -0,0 +1,7 @@ +package org.xbib.graphics.pdfbox.groovy.render.element + +import org.xbib.graphics.pdfbox.groovy.Line + +class LineElement { + Line node +} diff --git a/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/TextElement.groovy b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/TextElement.groovy new file mode 100644 index 0000000..0a7c409 --- /dev/null +++ b/graphics-pdfbox-groovy/src/main/groovy/org/xbib/graphics/pdfbox/groovy/render/element/TextElement.groovy @@ -0,0 +1,17 @@ +package org.xbib.graphics.pdfbox.groovy.render.element + +import org.apache.pdfbox.pdmodel.font.PDFont +import org.xbib.graphics.pdfbox.groovy.Text + +class TextElement { + + PDFont pdfFont + + Text node + + String text + + int width + + BigDecimal heightfactor +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BaseBuilderSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BaseBuilderSpec.groovy new file mode 100644 index 0000000..d48abf4 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BaseBuilderSpec.groovy @@ -0,0 +1,444 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.builder.DocumentBuilder +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +abstract class BaseBuilderSpec extends Specification { + + @Shared + ByteArrayOutputStream out + + @Shared + DocumentBuilder builder + + @Shared + byte[] imageData = getClass().classLoader.getResource('cat.jpg').bytes + + @Shared + testMargins = [ + [top: 0, bottom: 0, left: 0, right: 0], + [top: 2 * 72, bottom: 3 * 72, left: 1.25 * 72, right: 2.5 * 72], + [top: 72 / 4, bottom: 72 / 2, left: 72 / 4, right: 72 / 2] + ] + + byte[] getData() { out.toByteArray() } + + abstract DocumentBuilder getBuilderInstance(OutputStream out) + + abstract Document getDocument(byte[] data) + + def setup() { + out = new ByteArrayOutputStream() + builder = getBuilderInstance(out) + } + + @Unroll + def "set document margins"() { + when: + builder.create { + document(margin: [top: margin.top, bottom: margin.bottom, left: margin.left, right: margin.right]) { + paragraph 'Content' + } + } + + def document = getDocument(data) + + then: + document.margin.left == margin.left + + and: + document.margin.right == margin.right + + and: + document.margin.top == margin.top + + and: + document.margin.bottom == margin.bottom + + where: + margin << testMargins + } + + @Unroll + def "set paragraph margins"() { + when: + builder.create { + document { + paragraph(margin: currentMargin) { + text 'Foo' + } + } + } + + def paragraph = getDocument(data).children[0] + + then: + paragraph.margin.left == currentMargin.left + + and: + paragraph.margin.right >= currentMargin.right + + //and: + //paragraph.margin.top == currentMargin.top + + where: + currentMargin << testMargins + } + + def "create a simple table"() { + when: + builder.create { + document { + table { + row { + cell { + text 'FOOBAR' + } + } + } + } + } + + def table = getDocument(data).children[0] + + then: + table.children[0].children[0].children[0].text == 'FOOBAR' + } + + def "set table options"() { + when: + builder.create { + document { + table(width: 403.px, columns: [100.px, 300.px], border: [size: 1.px]) { + row { + cell('Cell 1') + cell('Cell 2') + } + } + } + } + + def table = getDocument(data).children[0] + + then: + table.width == 403 + + and: + table.children[0].children[0].width == 100 + + and: + table.children[0].children[1].width == 300 + } + + def "set paragraph text"() { + when: + builder.create { + document { + paragraph 'Foo' + paragraph('Foo') { + text 'Ba' + text 'r' + } + paragraph { + text 'B' + text 'a' + text 'r' + } + } + } + + def paragraphs = getDocument(data).children + + then: + paragraphs[0].text == 'Foo' + + and: + paragraphs[1].text == 'FooBar' + + and: + paragraphs[2].text == 'Bar' + } + + def "create a table with multiple columns"() { + when: + builder.create { + document { + table { + row { + cell 'Cell1' + cell 'Cell2' + cell { + text 'Cell3' + } + } + + } + } + } + + then: + notThrown(Exception) + } + + def "create a table with lots of rows"() { + when: + builder.create { + document { + table { + 50.times { i -> + row { + cell { + text 'TEST ' * (i + 1) + } + cell { + text 'FOO ' * (i + 1) + } + cell { + text 'BAR ' * (i + 1) + } + } + } + } + } + } + + then: + notThrown(Exception) + } + + def "add an image"() { + when: + builder.create { + document { + paragraph { + image(data: imageData, width: 500.px, height: 431.px) + } + } + } + + then: + notThrown(Exception) + } + + def "add a barcode"() { + when: + builder.create { + document { + paragraph { + barcode(height: 150.px, width: 500.px, value: '12345678') + } + } + } + + then: + notThrown(Exception) + } + + + def "paragraph header"() { + when: + builder.create { + document(header: { paragraph 'HEADER' }) { + paragraph 'Content' + } + } + + then: + notThrown(Exception) + } + + def "paragraph footer"() { + when: + builder.create { + document(footer: { paragraph 'FOOTER' }) { + paragraph 'Content' + } + } + + then: + notThrown(Exception) + } + + def "paragraph header and footer"() { + when: + builder.create { + document(header: { paragraph 'HEADER' }, footer: { paragraph 'FOOTER' }) { + paragraph 'Content' + } + } + + then: + notThrown(Exception) + } + + def "table header"() { + when: + builder.create { + document(header: { table { row { cell 'HEADER' } } }) { + paragraph 'Content' + } + } + + then: + notThrown(Exception) + } + + def "table footer"() { + when: + builder.create { + document(footer: { table { row { cell 'FOOTER' } } }) { + paragraph 'Content' + } + } + + then: + notThrown(Exception) + } + + /*def "table within table"() { + when: + builder.create { + document { + table { + row { + cell 'OUTER TABLE' + cell { + table { + row { + cell 'INNER TABLE' + } + } + } + } + } + } + } + + then: + notThrown(Exception) + }*/ + + def "table with rowspan"() { + when: + builder.create { + document { + table { + row { + cell 'FOO\nBAR', rowspan: 3 + cell('COL1-2') + } + row { + cell('COL2-1') + } + row { + cell('COL3-1') + } + row { + cell('COL4-1') + cell('COL4-2') + } + } + } + } + + then: + notThrown(Exception) + } + + def "table with line in cell paragraph"() { + when: + builder.create { + document { + table { + row { + cell { + line(startX: 1.cm, endX: 2.cm, strokewidth: 1.pt ) + } + } + } + } + } + + then: + notThrown(Exception) + } + + def "custom table with line"() { + when: + List layout = [ + [ key: 'Typ', value: 'Online', 'bold':true], + [ key: 'Medea-Nummer', value: 'test'], + [ key: 'Bestelldatum', value: 'test'], + [ key: 'Eingangsdatum', value: 'test'], + [ key: 'Besteller', value: 'test', line: true], + [ key: 'TAN', value: 'test'], + [ key: 'Benutzer', value: 'test'], + [ key: 'Kostenübernahme', value: 'test'], + [ key: 'Lieferart', value: 'test'], + [ key: 'Abholort', value: 'test'], + [ key: 'Abholcode', value: 'test'], + [ key: 'Buch/Zeitschrift', value: 'test'], + [ key: 'ISBN/ISSN', value: 'test'], + [ key: 'Quelle', value: 'test' ], + [ key: 'ID', value: 'test' ], + [ key: 'Erscheinungsort', value: 'test'], + [ key: 'Verlag', value: 'test'], + [ key: 'Aufsatztitel', value: 'test'], + [ key: 'Aufsatzautor', value: 'test'], + [ key: 'Jahrgang', value: 'test'], + [ key: 'Seitenangabe', value: 'test'], + [ key: 'Lieferant', value: 'test'], + [ key: 'Lieferantencode', value :'test' ], + [ key: 'Signatur/Standort', value : 'test' ] + ] + builder.create { + document(font: [family: 'Helvetica'], margin: [top: 1.cm]) { + paragraph(margin:[left: 6.cm, right: 1.cm, top: -1.5.cm]) { + font.size = 24.pt + font.bold = true + text 'Aufsatzbestellung Lieferschein' + } + paragraph { + table(margin:[left: 1.cm, top: 2.cm], width: 19.cm, padding: 0.pt, border:[size:0.pt]) { + layout.each { l -> + if (l.line) { + row { + cell(width: 19.cm) { + line(startX: 0.cm, endX: 19.cm, strokewidth: 1.pt) + } + } + } + row { + cell(width: 4.cm, align: 'left') { + if (l.bold) { + text l.key, font: [bold: true] + text ':', font: [bold: true] + } else { + text l.key + text ':' + } + } + cell(width: 15.cm, align: 'left') { + if (l.bold) { + text l.value, font: [bold: true] + } else { + text l.value + } + } + + } + } + } + } + } + } + + then: + notThrown(Exception) + + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BuilderSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BuilderSpec.groovy new file mode 100644 index 0000000..7e2128c --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BuilderSpec.groovy @@ -0,0 +1,557 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.Align +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Heading +import org.xbib.graphics.pdfbox.groovy.Line +import org.xbib.graphics.pdfbox.groovy.Row +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.Text +import org.xbib.graphics.pdfbox.groovy.TextBlock +import org.xbib.graphics.pdfbox.groovy.builder.DocumentBuilder +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +class BuilderSpec extends Specification { + + @Shared + DocumentBuilder builder + + @Shared + byte[] imageData = getClass().classLoader.getResource('cat.jpg').bytes + + + def setup() { + ByteArrayOutputStream out = new ByteArrayOutputStream() + builder = new TestBuilder(out) + } + + def "create empty document"() { + when: + def result = builder.create { + document() + } + + then: + result.document != null + + and: + result.document.getClass() == Document + } + + def "use typographic units"() { + when: + builder.create { + document(margin: [top: 2.inches, bottom: 1.inch]) { + paragraph(font: [size: 12.pt]) { + text 'Foo' + } + table(border: [size: 2.px]) { + row { + cell 'Bar' + } + } + } + } + + then: + notThrown(Exception) + } + + def 'onTextBlockComplete is called after a paragraph finishes'() { + def onTextBlockComplete = Mock(Closure) + builder.onTextBlockComplete = { TextBlock paragraph -> onTextBlockComplete(paragraph) } + + when: + builder.create { + document { + paragraph 'FOO BAR!' + } + } + + then: + 1 * onTextBlockComplete.call(_ as TextBlock) + } + + def 'onTableComple is called after table finishes'() { + def onTableComplete = Mock(Closure) + builder.onTableComplete = { Table table -> onTableComplete(table) } + + when: + builder.create { + document { + table { + row { + cell('Column1') + cell('Column2') + cell('Column3') + } + + } + } + } + + then: + 1 * onTableComplete.call(_ as Table) + } + + def 'onLineComplete is called after line finishes'() { + def onLineComplete = Mock(Closure) + builder.onLineComplete = { Line line -> onLineComplete(line) } + + when: + builder.create { + document { + table { + row { + cell { + paragraph { + line() + } + } + } + } + } + } + + then: + 1 * onLineComplete.call(_ as Line) + } + + def "Text element shouldn't have children"() { + when: + builder.create { + document { + paragraph { + text { + text 'FOOBAR!' + } + } + + } + } + + then: + thrown(Exception) + } + + def "LineBreak element shouldn't have children"() { + when: + builder.create { + document { + paragraph { + lineBreak { + lineBreak() + } + } + + } + } + + then: + thrown(Exception) + } + + def "Image element shouldn't have children"() { + when: + builder.create { + document { + paragraph { + image { + image() + } + } + + } + } + + then: + thrown(Exception) + } + + def "Line element shouldn't have children"() { + when: + builder.create { + document { + paragraph { + line { + line() + } + } + + } + } + + then: + thrown(Exception) + } + + + def "create a simple paragraph"() { + when: + def result = builder.create { + document { + paragraph 'FOO BAR!' + } + } + + TextBlock paragraph = result.document.children[0] + + then: + paragraph.text == 'FOO BAR!' + } + + def "create paragraphs with aligned text"() { + when: + def result = builder.create { + document { + paragraph 'default' + paragraph 'left', align: Align.LEFT + paragraph 'center', align: Align.CENTER + paragraph 'right', align: Align.RIGHT + } + } + + TextBlock paragraph1 = result.document.children[0] + TextBlock paragraph2 = result.document.children[1] + TextBlock paragraph3 = result.document.children[2] + TextBlock paragraph4 = result.document.children[3] + + then: + paragraph1.align == Align.LEFT + + and: + paragraph2.align == Align.LEFT + + and: + paragraph3.align == Align.CENTER + + and: + paragraph4.align == Align.RIGHT + } + + def "create paragraph with correct hierarchy"() { + when: + def result = builder.create { + document { + paragraph { + text 'FOO' + text 'BAR' + } + } + } + + Document document = result.document + TextBlock paragraph = document.children[0] + Text text1 = paragraph.children[0] + Text text2 = paragraph.children[1] + + then: + document.children == [paragraph] + + and: + paragraph.text == 'FOOBAR' + paragraph.children == [text1, text2] + + and: + paragraph.parent == document + + and: + text1.parent == paragraph + + and: + text2.parent == paragraph + } + + def "create table with the correct heirarchy"() { + when: + def result = builder.create { + document { + table { + row { + cell('FOO') + cell { + text 'BAR' + } + } + } + } + } + + Document document = result.document + Table table = document.children[0] + + Row row = table.children[0] + + Cell column1 = row.children[0] + Cell column2 = row.children[1] + + TextBlock paragraph1 = column1.children[0] + TextBlock paragraph2 = column2.children[0] + + Text text1 = paragraph1.children[0] + Text text2 = paragraph2.children[0] + + then: + table.parent == document + + and: + table.children == [row] + row.parent == table + row.children == [column1, column2] + + and: + column1.parent == row + column1.children == [paragraph1] + paragraph1.parent == column1 + + and: + column2.parent == row + column2.children == [paragraph2] + paragraph2.parent == column2 + + and: + text1.value == 'FOO' + text1.parent == paragraph1 + paragraph1.children == [text1] + + and: + text2.value == 'BAR' + text2.parent == paragraph2 + paragraph2.children == [text2] + } + + def "column widths are calculated"() { + when: + def result = builder.create { + document { + table(width: 250, padding: 0, border: [size: 0]) { + row { + cell 'FOOBAR' + cell 'BLAH' + } + } + } + } + + Table table = result.document.children[0] + Cell column1 = table.children[0].children[0] + Cell column2 = table.children[0].children[1] + + then: + table.width == 250 + + and: + column1.width == 125 + + and: + column2.width == 125 + } + + def "override or inherit font settings"() { + when: + def result = builder.create { + document(font: [family: 'Helvetica', color: '#121212']) { + + paragraph(font: [family: 'Courier', color: '#333333']) { + text 'Paragraph override' + } + paragraph 'Inherit doc font' + + paragraph { + text 'Text override', font: [family: 'Times-Roman', color: '#FFFFFF'] + } + + table(font: [family: 'Courier', color: '#111111']) { + row { + cell('Override') + } + } + + table { + row { + cell('Default font') + } + } + + } + } + + Document document = result.document + + def paragraph1 = document.children[0].children[0] + def paragraph2 = document.children[1].children[0] + def paragraph3 = document.children[2].children[0] + + def table1 = document.children[3].children[0].children[0].children[0] + def table2 = document.children[4].children[0].children[0].children[0] + + then: + paragraph1.font.family == 'Courier' + + and: + paragraph2.font.family == 'Helvetica' + + and: + paragraph3.font.family == 'Times-Roman' + + and: + table1.font.family == 'Courier' + + and: + table2.font.family == 'Helvetica' + } + + def "create a table with that contains an image and text"() { + when: + builder.create { + document { + table { + row { + cell { + image(data: imageData, width: 500.px, height: 431.px) + lineBreak() + text 'A cat' + } + } + + } + } + } + + then: + notThrown(Exception) + } + + def "background color cascades"() { + given: + String[] backgroundColors = ['#000000', '#111111', '#333333'] + + when: + Document result = builder.create { + document { + table(background: backgroundColors[0]) { + row { + cell '1.1' + cell '1.2' + } + } + + table { + row(background: backgroundColors[1]) { + cell '2.1' + cell '2.2' + } + } + + table { + row { + cell '3-1', background: backgroundColors[2] + cell '3-2' + } + } + } + }.document + + Table table1 = result.children[0] + Table table2 = result.children[1] + Table table3 = result.children[2] + + then: + table1.background.hex == backgroundColors[0] - '#' + table1.children[0].background.hex == backgroundColors[0] - '#' + table1.children[0].children.each { Cell column -> + assert column.background.hex == backgroundColors[0] - '#' + } + + and: + table2.background == null + table2.children[0].background.hex == backgroundColors[1] - '#' + table2.children[0].children.each { Cell column -> + assert column.background.hex == backgroundColors[1] - '#' + } + + and: + table3.background == null + table3.children[0].background == null + table3.children[0].children[0].background.hex == backgroundColors[2] - '#' + table3.children[0].children[1].background == null + } + + def "set link on linkable nodes"() { + String url = 'http://www.craigburke.com' + + when: + Document result = builder.create { + document { + heading1 'HEADING1', url: url + heading2 'HEADING2' + + paragraph 'Paragraph1', url: url + paragraph { + text 'Check this out: ' + text 'Click on me', url: url + } + } + }.document + + Heading heading1 = result.children[0] + Heading heading2 = result.children[1] + TextBlock paragraph1 = result.children[2] + TextBlock paragraph2 = result.children[3] + + then: + heading1.url == url + + and: + heading2.url == null + + and: + paragraph1.url == url + paragraph1.children[0].url == url + + and: + paragraph2.url == null + paragraph2.children[0].url == null + paragraph2.children[1].url == url + } + + @Unroll('Template keys calculated for #description') + def "template keys are calculated"() { + expect: + TestBuilder.getTemplateKeys(node, nodeKey) == expectedKeys.toArray() + + where: + node | nodeKey || expectedKeys + new TextBlock() | 'paragraph' || ['paragraph'] + new TextBlock(style: 'foo') | 'paragraph' || ['paragraph', 'paragraph.foo'] + + new Text() | 'text' || ['text'] + new Text(style: 'bar') | 'text' || ['text', 'text.bar'] + + new Table() | 'table' || ['table'] + new Table(style: 'foo') | 'table' || ['table', 'table.foo'] + new Row() | 'row' || ['row'] + new Row(style: 'foo') | 'row' || ['row', 'row.foo'] + new Cell() | 'cell' || ['cell'] + new Cell(style: 'foo') | 'cell' || ['cell', 'cell.foo'] + + new Heading(level: 1) | 'heading' || ['heading', 'heading1'] + new Heading(level: 1, style: 'foobar') | 'heading' || ['heading', 'heading1', 'heading.foobar', 'heading1.foobar'] + new Heading(level: 2) | 'heading' || ['heading', 'heading2'] + new Heading(level: 2, style: 'foobar') | 'heading' || ['heading', 'heading2', 'heading.foobar', 'heading2.foobar'] + new Heading(level: 3) | 'heading' || ['heading', 'heading3'] + new Heading(level: 3, style: 'foobar') | 'heading' || ['heading', 'heading3', 'heading.foobar', 'heading3.foobar'] + new Heading(level: 4) | 'heading' || ['heading', 'heading4'] + new Heading(level: 4, style: 'foobar') | 'heading' || ['heading', 'heading4', 'heading.foobar', 'heading4.foobar'] + new Heading(level: 5) | 'heading' || ['heading', 'heading5'] + new Heading(level: 5, style: 'foobar') | 'heading' || ['heading', 'heading5', 'heading.foobar', 'heading5.foobar'] + new Heading(level: 6) | 'heading' || ['heading', 'heading6'] + new Heading(level: 6, style: 'foobar') | 'heading' || ['heading', 'heading6', 'heading.foobar', 'heading6.foobar'] + + description = "${nodeKey}${node.style ? ".${node.style}" : ''}" + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ColorSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ColorSpec.groovy new file mode 100644 index 0000000..429dde5 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ColorSpec.groovy @@ -0,0 +1,48 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.Color +import spock.lang.Specification +import spock.lang.Unroll + +class ColorSpec extends Specification { + + static final BLACK_RGB = [0, 0, 0] + + @Unroll + def "set color"() { + Color color = new Color() + + when: + color.color = rgb + + then: + color.rgb == rgb + color.hex == hex - '#' + + where: + hex | rgb + '000000' | BLACK_RGB + '#000000' | BLACK_RGB + } + + def "shouldn't be able to directly set hex"() { + Color color = new Color() + + when: + color.hex = BLACK_RGB + + then: + thrown(UnsupportedOperationException) + } + + def "shouldn't be able to directly set rgb"() { + Color color = new Color() + + when: + color.rgb = '000000' + + then: + thrown(UnsupportedOperationException) + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/DocumentAnalyzerTest.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/DocumentAnalyzerTest.groovy new file mode 100644 index 0000000..18c463e --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/DocumentAnalyzerTest.groovy @@ -0,0 +1,28 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import groovy.util.logging.Log4j2 +import org.junit.Test +import org.xbib.graphics.pdfbox.groovy.analyze.DocumentAnalyzer + +@Log4j2 +class DocumentAnalyzerTest { + + @Test + void analyze() { + InputStream inputStream = getClass().getResourceAsStream("/ghost.pdf") + if (inputStream) { + DocumentAnalyzer documentAnalyzer = new DocumentAnalyzer(inputStream) + log.info(documentAnalyzer.result) + } + } + + @Test(expected = IOException.class) + void analyzeNonPDF() { + InputStream inputStream = getClass().getResourceAsStream("/log4j2-test.xml") + if (inputStream) { + DocumentAnalyzer documentAnalyzer = new DocumentAnalyzer(inputStream) + log.info(documentAnalyzer.result) + } + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/FontSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/FontSpec.groovy new file mode 100644 index 0000000..f122876 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/FontSpec.groovy @@ -0,0 +1,52 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import groovy.util.logging.Log4j2 +import org.xbib.graphics.pdfbox.groovy.Font +import org.xbib.graphics.pdfbox.groovy.builder.PdfFont +import spock.lang.Specification + +@Log4j2 +class FontSpec extends Specification { + + def "override properties with left shift"() { + Font font = new Font(family:'Initial', size:10) + + when: + font << [family:'New'] + + then: + font.family == 'New' + font.size == 10 + + when: + font << [size:12] + + then: + font.family == 'New' + font.size == 12 + + } + + def "printable characters"() { + String s = "\u0098 Hello Jörg" + + when: + s = s.replaceAll("\\p{C}", "") + + then: + s == " Hello Jörg" + } + + def "glyph exists"() { + String string = "Jörg \\u010d" + Font font = new Font(family: 'Helvetica') + Boolean b = false + + when: + b = PdfFont.canEncode(font, string) + + then: + log.info("b=${b}") + b + } +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/LoadFontTest.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/LoadFontTest.groovy new file mode 100644 index 0000000..0ea9e96 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/LoadFontTest.groovy @@ -0,0 +1,66 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import groovy.util.logging.Log4j2 +import org.apache.fontbox.ttf.CmapSubtable +import org.apache.fontbox.ttf.NamingTable +import org.apache.fontbox.ttf.TTFParser +import org.apache.fontbox.ttf.TrueTypeFont +import org.junit.Test + +import java.nio.file.Files +import java.nio.file.Paths + +@Log4j2 +class LoadFontTest { + + @Test + void testOpenTypeFont() { + def names = ['NotoSansCJKtc-Regular.ttf', 'NotoSansCJKtc-Bold.ttf'] + names.each { name -> + InputStream inputStream = Files.newInputStream(Paths.get("src/test/resources/fonts/" + name)) + inputStream.withCloseable { + addOpenTypeFont(name, inputStream) + } + } + } + + private final Map ttf = new HashMap<>() + private final Map otf = new HashMap<>() + + private void addOpenTypeFont(String name, InputStream inputStream) { + TTFParser ttfParser = new TTFParser(false, true) + TrueTypeFont trueTypeFont = ttfParser.parse(inputStream) + try { + NamingTable nameTable = trueTypeFont.getNaming() + if (!nameTable) { + log.warn("Missing 'name' table in font " + name) + } else { + if (nameTable.getPostScriptName()) { + String psName = nameTable.getPostScriptName() + String format + if (trueTypeFont.getTableMap().get("CFF ")) { + format = "OTF" + otf.put(psName, trueTypeFont); + } else { + format = "TTF"; + ttf.put(psName, trueTypeFont) + } + log.info(format + ": '" + psName + "' / '" + nameTable.getFontFamily() + + "' / '" + nameTable.getFontSubFamily() + "'") + } else { + log.warn("Missing 'name' entry for PostScript name in font " + inputStream) + } + } + CmapSubtable cmapSubtable = trueTypeFont.getUnicodeCmap(true) + if (!cmapSubtable) { + log.warn('missing cmap table in ' + name) + } else { + log.info("cmap table present: " + name) + } + } finally { + if (trueTypeFont != null) { + trueTypeFont.close() + } + } + } +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ParagraphRendererSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ParagraphRendererSpec.groovy new file mode 100644 index 0000000..6b1c494 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ParagraphRendererSpec.groovy @@ -0,0 +1,90 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.TextBlock +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument +import org.xbib.graphics.pdfbox.groovy.render.ParagraphRenderer +import spock.lang.Shared + +class ParagraphRendererSpec extends RendererTestBase { + + @Shared + ParagraphRenderer paragraphElement + + def setup() { + TextBlock paragraph = makeParagraph(3) + Document document = makeDocument() + PdfDocument pdfDocument = new PdfDocument(document) + paragraphElement = makeParagraphElement(pdfDocument, paragraph) + } + + def cleanup() { + paragraphElement.pdfDocument.getPdDocument().close() + } + + def "Can parse all lines"() { + float height = defaultLineHeight * 3f + + when: + paragraphElement.parse(height) + + then: + paragraphElement.lines.size() == 3 + + and: + paragraphElement.parseStart == 0 + + and: + paragraphElement.parseEnd == 2 + + and: + paragraphElement.fullyParsed + } + + def "Can parse a single line"() { + when: + paragraphElement.with { + parse(defaultLineHeight) + } + + then: + paragraphElement.parseStart == 0 + + and: + paragraphElement.parseEnd == 0 + + and: + paragraphElement.linesParsed == 1 + + when: + paragraphElement.with { + render(0, 0) + parse(defaultLineHeight) + } + + then: + paragraphElement.parseStart == 1 + + and: + paragraphElement.parseEnd == 1 + + when: + paragraphElement.with { + render(0, 0) + parse(defaultLineHeight) + render(0, 0) + } + + then: + paragraphElement.parseStart == 2 + + and: + paragraphElement.parseEnd == 2 + + and: + paragraphElement.fullyParsed + + and: + paragraphElement.fullyRendered + } +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ParagraphSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ParagraphSpec.groovy new file mode 100644 index 0000000..bcf536a --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/ParagraphSpec.groovy @@ -0,0 +1,31 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.LineBreak +import org.xbib.graphics.pdfbox.groovy.Text +import org.xbib.graphics.pdfbox.groovy.TextBlock +import spock.lang.Shared +import spock.lang.Specification + +class ParagraphSpec extends Specification { + + @Shared TextBlock paragraph + static final int DEFAULT_FONT_SIZE = 12 + + def setup() { + paragraph = new TextBlock() + paragraph.children << new Text(font:[size:DEFAULT_FONT_SIZE]) + } + + def "text combines text values"() { + paragraph.children = [ + new Text(value:'FOO'), + new Text(value:'BAR'), + new LineBreak(), + new Text(value:'123') + ] + + expect: + paragraph.text == 'FOOBAR123' + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfContentExtractor.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfContentExtractor.groovy new file mode 100644 index 0000000..6d9778a --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfContentExtractor.groovy @@ -0,0 +1,128 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.apache.pdfbox.pdmodel.PDPage +import org.apache.pdfbox.pdmodel.PDPageTree +import org.apache.pdfbox.pdmodel.common.PDRectangle +import org.apache.pdfbox.text.PDFTextStripper +import org.apache.pdfbox.text.TextPosition +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Font +import org.xbib.graphics.pdfbox.groovy.Text +import org.xbib.graphics.pdfbox.groovy.TextBlock + +class PdfContentExtractor extends PDFTextStripper { + + private tablePosition = [row: 0, cell: 0] + private int currentChildNumber = 0 + private Document doc + private TextPosition lastPosition + private PDRectangle pageSize + + PdfContentExtractor(Document doc) { + super.setSortByPosition(true) + this.doc = doc + this.document = doc.element + } + + private getCurrentChild() { + if (!doc.children || doc.children.size() < currentChildNumber) { + null + } else { + doc.children[currentChildNumber - 1] + } + } + + @Override + protected void processPages(PDPageTree pages) throws IOException { + super.processPages(pages) + } + + @Override + public void processPage(PDPage page) throws IOException { + this.pageSize = page.getCropBox() + super.processPage(page) + } + + @Override + protected void writePage() throws IOException { + // disabled + } + + @Override + void processTextPosition(TextPosition text) { + updateChildNumber(text) + Font currentFont = new Font(family: text.font.fontDescriptor.fontFamily, size: text.fontSize) + def textNode + if (currentChild.getClass() == TextBlock) { + textNode = processParagraph(text, currentFont) + } else { + textNode = processTable(text, currentFont) + } + textNode?.value += text.unicode + lastPosition = text + } + + private processTable(TextPosition text, Font font) { + def textNode + Cell cell = currentChild.children[tablePosition.row].children[tablePosition.cell] + TextBlock paragraph = cell.children[0] + paragraph.font = paragraph.font ?: font + if (!paragraph.children || isNewSection(text)) { + textNode = getText(paragraph, font) + paragraph.children << textNode + } else { + textNode = paragraph.children.last() + } + textNode + } + + private processParagraph(TextPosition text, Font font) { + def textNode + if (!currentChild.children) { + textNode = getText(currentChild, font) + currentChild.children << textNode + setParagraphProperties(currentChild, text, font) + } else if (isNewSection(text)) { + textNode = getText(currentChild, font) + currentChild.children << textNode + } else { + textNode = currentChild.children.last() + } + textNode + } + + private void setParagraphProperties(paragraph, TextPosition text, Font font) { + paragraph.font = font.clone() + paragraph.margin.left = text.x - doc.margin.left + int totalPageWidth = pageSize.getWidth() - doc.margin.right - doc.margin.left + paragraph.margin.right = totalPageWidth - text.width - paragraph.margin.left + int topMargin = Math.ceil(text.y - doc.margin.top) + paragraph.margin.top = Math.round(topMargin) + } + + private Text getText(paragraph, Font font) { + new Text(parent: paragraph, value: '', font: font) + } + + private void updateChildNumber(TextPosition current) { + if (!lastPosition || (lastPosition.y != current.y)) { + currentChildNumber++ + tablePosition.row = 0 + tablePosition.cell = 0 + } + } + + private boolean isNewSection(TextPosition current) { + boolean isNewSection = false + if (!lastPosition) { + isNewSection = true + } else if (current.font != lastPosition.font) { + isNewSection = true + } else if (current.fontSizeInPt != lastPosition.fontSizeInPt) { + isNewSection = true + } + isNewSection + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentBuilderSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentBuilderSpec.groovy new file mode 100644 index 0000000..e44e308 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentBuilderSpec.groovy @@ -0,0 +1,16 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.builder.DocumentBuilder +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocumentBuilder + +class PdfDocumentBuilderSpec extends BaseBuilderSpec { + + DocumentBuilder getBuilderInstance(OutputStream out) { + new PdfDocumentBuilder(out) + } + + Document getDocument(byte[] data) { + PdfDocumentLoader.load(data) + } +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentBuilderTest.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentBuilderTest.groovy new file mode 100644 index 0000000..fd81196 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentBuilderTest.groovy @@ -0,0 +1,223 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import groovy.util.logging.Log4j2 +import org.junit.Test +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocumentBuilder + +import java.nio.file.Files +import java.nio.file.Paths +import java.util.regex.Pattern + +@Log4j2 +class PdfDocumentBuilderTest { + + @Test + void testPdfDocumentBuilderCJK() { + List fontdefs = [ + [name: 'Noto Sans CJK TC Regular', resource: 'fonts/NotoSansCJKtc-Regular.ttf', bold: false, italic: false] + ] + OutputStream outputStream = Files.newOutputStream(Paths.get("build/testcjk.pdf")) + outputStream.withCloseable { + PdfDocumentBuilder builder = new PdfDocumentBuilder(outputStream, fontdefs) + builder.create { + document(font: [family: 'Noto Sans CJK TC Regular', color: '#000000', size: 12.pt]) { + paragraph "北京 東京大学" + } + } + } + } + + @Test + void testPdfDocumentBuilderA6LandscapePaperSize() { + OutputStream outputStream = Files.newOutputStream(Paths.get("build/testa6.pdf")) + outputStream.withCloseable { + PdfDocumentBuilder builder = new PdfDocumentBuilder(outputStream) + builder.create { + document(papersize: 'A6', orientation: 'landscape') { + paragraph "Hello World" + } + } + } + } + + @Test + void testTableDocument() { + OutputStream outputStream = Files.newOutputStream(Paths.get("build/testtable.pdf")) + outputStream.withCloseable { + PdfDocumentBuilder builder = new PdfDocumentBuilder(outputStream) + List layout = [ + [key: 'Typ', value: 'Online', 'bold': true], + [key: 'Medea-Nummer', value: 'test'], + [key: 'Bestelldatum', value: 'test'], + [key: 'Eingangsdatum', value: 'test'], + [key: 'Besteller', value: 'test', line: true], + [key: 'TAN', value: 'test'], + [key: 'Benutzer', value: 'test'], + [key: 'Kostenübernahme', value: 'test'], + [key: 'Lieferart', value: 'test'], + [key: 'Abholort', value: 'test'], + [key: 'Abholcode', value: 'test'], + [key: 'Buch/Zeitschrift', value: 'test'], + [key: 'ISBN/ISSN', value: 'test'], + [key: 'Quelle', value: 'test'], + [key: 'ID', value: 'test'], + [key: 'Erscheinungsort', value: 'test'], + [key: 'Verlag', value: 'test'], + [key: 'Aufsatztitel', value: 'test'], + [key: 'Aufsatzautor', value: 'test'], + [key: 'Jahrgang', value: 'test'], + [key: 'Seitenangabe', value: 'test'], + [key: 'Lieferant', value: 'test', line: 'true', bold: 'true'], + [key: 'Lieferantencode', value: 'test', bold: 'true'], + [key: 'Signatur/Standort', value: 'test', bold: 'true'] + ] + builder.create { + document(font: [family: 'Helvetica'], margin: [top: 1.cm]) { + paragraph(margin: [left: 6.cm, right: 1.cm, top: 0.cm]) { + font.size = 24.pt + font.bold = true + text 'Table Test Document' + } + paragraph { + table(margin: [left: 1.cm, top: 2.cm], width: 19.cm, padding: 0.pt, border: [size: 0.pt]) { + layout.each { l -> + if (l.line) { + row { + cell(width: 19.cm) { + line(startX: 0.cm, endX: 19.cm, startY: 6.pt, strokewidth: 0.5f) + } + } + } + row { + cell(width: 4.cm, align: 'left') { + if (l.bold) { + text l.key, font: [bold: true] + text ':', font: [bold: true] + } else { + text l.key + text ':' + } + } + cell(width: 15.cm, align: 'left') { + if (l.bold) { + text l.value, font: [bold: true], heightfactor: 2 + } else { + text l.value, heightfactor : 2 + } + } + } + } + } + } + } + } + } + } + + @Test + void testPdfWithImage() { + byte[] logo = getClass().getResourceAsStream('/img/logo-print.png').bytes + OutputStream outputStream = Files.newOutputStream(Paths.get("build/testimage.pdf")) + outputStream.withCloseable { + PdfDocumentBuilder builder = new PdfDocumentBuilder(outputStream) + List layout = [ + [key: 'Typ', value: 'Online', 'bold': true] + ] + builder.create { + document(font: [family: 'Helvetica'], margin: [top: 1.cm]) { + paragraph(margin: [left: 7.mm]) { + image(data: logo, name: 'logo-print.png', width: 125.px, height: 45.px) + } + paragraph(margin: [left: 6.cm, right: 1.cm, top: 0.cm]) { + font.size = 24.pt + font.bold = true + text 'Table Test Document' + } + paragraph { + table(margin: [left: 1.cm, top: 2.cm], width: 19.cm, padding: 0.pt, border: [size: 0.pt]) { + layout.each { l -> + if (l.line) { + row { + cell(width: 19.cm) { + line(startX: 0.cm, endX: 19.cm, startY: 6.pt, strokewidth: 0.5f) + } + } + } + row { + cell(width: 4.cm, align: 'left') { + if (l.bold) { + text l.key, font: [bold: true] + text ':', font: [bold: true] + } else { + text l.key + text ':' + } + } + cell(width: 15.cm, align: 'left') { + if (l.bold) { + text l.value, font: [bold: true], heightfactor: 2 + } else { + text l.value, heightfactor : 2 + } + } + } + } + } + } + } + } + } + } + + @Test + void testPdfWithBarcode() { + OutputStream outputStream = Files.newOutputStream(Paths.get('build/barcode.pdf')) + outputStream.withCloseable { + PdfDocumentBuilder builder = new PdfDocumentBuilder(outputStream) + builder.create { + document { + paragraph { + text "Hello World" + barcode(x: 10.cm, y: 1.cm, width: 6.cm, height: 2.cm, value: '20180123456') + } + paragraph "Hello World bottom" + } + } + } + } + + @Test + void testPdfWithCaron() { + List fontdefs = [ + [name: 'Noto Sans Regular', resource: 'fonts/NotoSans-Regular.ttf', bold: false, italic: false], + [name: 'Noto Sans Bold', resource: 'fonts/NotoSans-Bold.ttf', bold: true, italic: false], + [name: 'Noto Sans CJK TC Regular', resource: 'fonts/NotoSansCJKtc-Regular.ttf', bold: false, italic: false], + [name: 'Noto Sans CJK TC Bold', resource: 'fonts/NotoSansCJKtc-Bold.ttf', bold: true, italic: false] + ] + OutputStream outputStream = Files.newOutputStream(Paths.get('build/caron.pdf')) + outputStream.withCloseable { + PdfDocumentBuilder builder = new PdfDocumentBuilder(outputStream, fontdefs) + builder.create { + document { + paragraph { + font.family = 'Noto Sans Regular' + text "Latin Small Letter C with Caron \u010d" + } + } + } + } + } + + @Test + void testCharacters() { + Pattern detectHan = Pattern.compile('.*\\p{script=Han}.*') + Pattern detectLatin = Pattern.compile('.*\\p{script=Latin}.*') + String chinese = "北京 東京大学" + String caron = "Hey Jörg, this is Latin Small Letter C with Caron \u010d" + log.info("chinese = ${detectHan.matcher(chinese).matches()}") + log.info("caron = ${detectLatin.matcher(caron).matches()}") + String normalized = caron.replaceAll("\\P{IsLatin}","") + log.info("normalized=${normalized}") + + } +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentLoader.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentLoader.groovy new file mode 100644 index 0000000..1cef31d --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/PdfDocumentLoader.groovy @@ -0,0 +1,62 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.apache.pdfbox.pdmodel.PDDocument +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Image +import org.xbib.graphics.pdfbox.groovy.Row +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.TextBlock + +class PdfDocumentLoader { + + static Document load(byte[] data) { + PDDocument pdfDoc = PDDocument.load(new ByteArrayInputStream(data)) + Document document = new Document(element: pdfDoc) + def metaData = new XmlParser().parse(pdfDoc.documentCatalog.metadata.createInputStream()) + document.margin.top = metaData.'@marginTop' as Integer + document.margin.bottom = metaData.'@marginBottom' as Integer + document.margin.left = metaData.'@marginLeft' as Integer + document.margin.right = metaData.'@marginRight' as Integer + metaData.each { + if (it.name() == 'paragraph') { + loadParagraph(document, it) + } else if (it.name() == 'table') { + loadTable(document, it) + } else { + throw new IOException('unknown metadata name ' + it.name()) + } + } + def extractor = new PdfContentExtractor(document) + extractor.processPages(pdfDoc.getPages()) + pdfDoc.close() + document + } + + private static loadParagraph(Document document, paragraphNode) { + def paragraph = new TextBlock(parent: document) + paragraph.margin.top = paragraphNode.'@marginTop' as Integer + paragraph.margin.bottom = paragraphNode.'@marginBottom' as Integer + paragraph.margin.left = paragraphNode.'@marginLeft' as Integer + paragraph.margin.right = paragraphNode.'@marginRight' as Integer + paragraphNode.image.each { + paragraph.children << new Image(parent: paragraph) + } + document.children << paragraph + } + + private static loadTable(Document document, tableNode) { + def table = new Table(parent: document, width: tableNode.'@width' as Integer) + tableNode.row.each { rowNode -> + Row row = new Row() + rowNode.cell.each { cellNode -> + def cell = new Cell(width: cellNode.'@width' as Integer) + cell.children << new TextBlock() + row.children << cell + } + table.children << row + } + document.children << table + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/RendererTestBase.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/RendererTestBase.groovy new file mode 100644 index 0000000..e2f50d2 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/RendererTestBase.groovy @@ -0,0 +1,52 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.BaseNode +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Font +import org.xbib.graphics.pdfbox.groovy.LineBreak +import org.xbib.graphics.pdfbox.groovy.Margin +import org.xbib.graphics.pdfbox.groovy.Text +import org.xbib.graphics.pdfbox.groovy.TextBlock +import org.xbib.graphics.pdfbox.groovy.UnitUtil +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument +import org.xbib.graphics.pdfbox.groovy.render.ParagraphRenderer +import spock.lang.Specification + +class RendererTestBase extends Specification { + + public static final Margin defaultMargin = new Margin(top: UnitUtil.mmToPoint(5 as BigDecimal), + bottom: UnitUtil.mmToPoint(5 as BigDecimal), + left: UnitUtil.mmToPoint(5 as BigDecimal), + right: UnitUtil.mmToPoint(5 as BigDecimal)) + + public static final BigDecimal defaultLineHeight = 18 as BigDecimal + + Document makeDocument() { + new Document(margin: defaultMargin, font: new Font()) + } + + TextBlock makeParagraph(TextBlock paragraph, BaseNode parent = makeDocument()) { + TextBlock newParagraph = paragraph.clone() + newParagraph.parent = parent + parent.children << newParagraph + newParagraph + } + + TextBlock makeParagraph(int lineCount, BaseNode parent = makeDocument()) { + TextBlock paragraph = new TextBlock(margin: Margin.NONE, font: new Font()) + lineCount.times { + paragraph.children << new Text(value: "Line${it}", font: new Font()) + if (it != lineCount - 1) { + paragraph.children << new LineBreak() + } + } + paragraph.parent = parent + parent.children << paragraph + paragraph + } + + ParagraphRenderer makeParagraphElement(PdfDocument pdfDocument, TextBlock paragraph) { + new ParagraphRenderer(paragraph, pdfDocument, 0 as BigDecimal, 0 as BigDecimal, paragraph.parent.width) + } + +} \ No newline at end of file diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/RowRendererSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/RowRendererSpec.groovy new file mode 100644 index 0000000..835e00a --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/RowRendererSpec.groovy @@ -0,0 +1,125 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Row +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument +import org.xbib.graphics.pdfbox.groovy.render.CellRenderer +import org.xbib.graphics.pdfbox.groovy.render.RowRenderer +import spock.lang.Shared + +class RowRendererSpec extends RendererTestBase { + + @Shared + List rowRenderers + + def setup() { + rowRenderers = [] + Document document = makeDocument() + Table table = new Table(width: 800, padding: 0, border: [size: 0]) + table.parent = document + document.children << table + + 2.times { addRow(table) } + + PdfDocument pdfDocument = new PdfDocument(document) + table.children.each { + rowRenderers << new RowRenderer(it, pdfDocument, 0, 0) + } + } + + void cleanup() { + rowRenderers.each { + it.getPdfDocument().pdDocument.close() + } + } + + private void addRow(Table table) { + Row row = new Row(parent: table) + table.children << row + Cell rowspanCell = new Cell(parent: row, width: 200, rowspan: 2) + Cell normalCell = new Cell(parent: row, width: 100) + makeParagraph(5, rowspanCell) + makeParagraph(5, normalCell) + row.children << rowspanCell + row.children << normalCell + } + + def "rowspan height is set correctly after multiple parses"() { + RowRenderer rowRenderer = rowRenderers[0] + CellRenderer cellRenderer = rowRenderer.cellRenderers[0] + + when: + BigDecimal parseHeight = defaultLineHeight * 3 + rowRenderer.parse(parseHeight) + + then: + cellRenderer.rowspanHeight == parseHeight + + when: + parseHeight = defaultLineHeight * 2 + rowRenderer.parse(parseHeight) + + then: + cellRenderer.rowspanHeight == parseHeight + + when: + parseHeight = defaultLineHeight + rowRenderer.parse(parseHeight) + + then: + cellRenderer.rowspanHeight == parseHeight + } + + def "rowspan height is updated after render"() { + RowRenderer rowRenderer = rowRenderers[0] + CellRenderer cellRenderer = rowRenderer.cellRenderers[0] + + when: + rowRenderer.parse(defaultLineHeight) + rowRenderer.render(0, 0) + + then: + cellRenderer.rowspanHeight == defaultLineHeight + + when: + rowRenderer.parse(defaultLineHeight) + + then: + cellRenderer.currentRowHeight == defaultLineHeight + + and: + cellRenderer.rowspanHeight == (defaultLineHeight * 2) + + when: + rowRenderer.render(0, 0) + + then: + cellRenderer.rowspanHeight == (defaultLineHeight * 2) + } + + def "parsedHeight is set correctly"() { + RowRenderer rowRenderer = rowRenderers[0] + CellRenderer cellRenderer = rowRenderer.cellRenderers[0] + + when: + BigDecimal partialHeight = defaultLineHeight * 3 + rowRenderer.parse(partialHeight) + rowRenderer.render(0, 0) + + then: + cellRenderer.parsedHeight == 0 + + and: + cellRenderer.rowspanHeight == partialHeight + + when: + rowRenderer.parse(defaultLineHeight) + + then: + cellRenderer.parsedHeight == 0 + cellRenderer.rowspanHeight == partialHeight + defaultLineHeight + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TableRendererSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TableRendererSpec.groovy new file mode 100644 index 0000000..df34c7e --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TableRendererSpec.groovy @@ -0,0 +1,101 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.Cell +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Font +import org.xbib.graphics.pdfbox.groovy.Margin +import org.xbib.graphics.pdfbox.groovy.Row +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.TextBlock +import org.xbib.graphics.pdfbox.groovy.builder.PdfDocument +import org.xbib.graphics.pdfbox.groovy.render.TableRenderer +import spock.lang.Shared + +class TableRendererSpec extends RendererTestBase { + + @Shared Table table + + @Shared TableRenderer tableRenderer + + @Shared BigDecimal defaultRowHeight + + @Shared int rowCount = 2 + + def setup() { + table = new Table(margin: Margin.NONE, padding:20, border:[size:3], columns:[1]) + TextBlock paragraph = makeParagraph(5) + paragraph.margin = Margin.NONE + tableRenderer = makeTableElement(table, paragraph, rowCount) + defaultRowHeight = (defaultLineHeight * 5) + (table.padding * 2) + (table.border.size) + } + + def cleanup() { + tableRenderer.pdfDocument.close() + } + + def "parse first row"() { + BigDecimal firstRowHeight = defaultRowHeight + table.border.size + + when: + tableRenderer.parse(firstRowHeight) + + then: + tableRenderer.parsedHeight == firstRowHeight + + and: + tableRenderer.parseStart == 0 + + and: + tableRenderer.parseEnd == 0 + } + + def "parse part of first row"() { + BigDecimal partialRowHeight = table.padding + (defaultLineHeight * 3) + table.border.size + + when: + tableRenderer.parse(partialRowHeight) + + then: + tableRenderer.parseStart == 0 + + and: + tableRenderer.parseEnd == 0 + + and: + tableRenderer.parsedHeight == partialRowHeight + } + + def "parse all rows"() { + BigDecimal totalHeight = (rowCount * defaultRowHeight) + table.border.size + + when: + tableRenderer.parse(totalHeight) + + then: + tableRenderer.parsedHeight == totalHeight + + and: + tableRenderer.fullyParsed + } + + private TableRenderer makeTableElement(Table table, TextBlock paragraph, int rows) { + Document tableDocument = makeDocument() + table.parent = tableDocument + int cellCount = table.columns.size() + rows.times { + Row row = new Row(font:new Font()) + row.parent = table + table.children << row + cellCount.times { + Cell cell = new Cell(font:new Font()) + row.children << cell + cell.parent = row + makeParagraph(paragraph, cell) + } + } + table.updateRowspanColumns() + table.normalizeColumnWidths() + PdfDocument pdfDocument = new PdfDocument(tableDocument) + new TableRenderer(table, pdfDocument, 0 as BigDecimal, 0 as BigDecimal) + } +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TableSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TableSpec.groovy new file mode 100644 index 0000000..548fdc9 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TableSpec.groovy @@ -0,0 +1,190 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.Row +import org.xbib.graphics.pdfbox.groovy.Table +import org.xbib.graphics.pdfbox.groovy.builder.DocumentBuilder +import spock.lang.Shared +import spock.lang.Specification + +class TableSpec extends Specification { + + @Shared + DocumentBuilder builder + + def setup() { + ByteArrayOutputStream out = new ByteArrayOutputStream() + builder = new TestBuilder(out) + } + + def "table within a table"() { + when: + Document result = builder.create { + document { + table { + row { + cell 'OUTER TABLE' + cell { + table { + row { + cell 'INNER TABLE' + } + } + } + } + } + } + }.document + + Table outerTable = result.children[0] + Table innerTable = outerTable.children[0].children[1].children[0] + + then: + outerTable.children[0].children[0].children[0].text == 'OUTER TABLE' + + and: + innerTable.children[0].children[0].children[0].text == 'INNER TABLE' + } + + def "widths are set correct with table within a table"() { + when: + Document result = builder.create { + document { + table(width: 450, border:[size:0], columns:[200, 250]) { + row { + cell { + table(width: 400, padding: 0) { + row { + cell 'INNER TABLE' + } + } + } + cell() + } + } + } + }.document + + Table outerTable = result.children[0] + Table innerTable = outerTable.children[0].children[0].children[0] + + then: + outerTable.width == 450 + + and: + outerTable.children[0].children[0].width == 200 + + and: + innerTable.width == 180 + } + + def "widths are set correctly with table that uses colspans"() { + when: + Document result = builder.create { + document { + table(width: 450, border:[size:0], columns: [200, 100, 150]) { + row { + cell(colspan:2) + cell() + } + } + } + }.document + + Table table = result.children[0] + Row row = table.children[0] + + then: + table.width == 450 + + and: + row.children[0].width == 300 + + and: + row.children[1].width == 150 + } + + def "columns are repeated when rowspan is specified"() { + when: + Document result = builder.create { + document { + table { + row { + cell(rowspan:3) + cell() + cell() + } + row { + cell() + cell() + } + row { + cell() + cell() + } + row { + cell() + cell() + cell() + } + } + } + }.document + + Table table = result.children[0] + Row row1 = table.children[0] + Row row2 = table.children[1] + Row row3 = table.children[2] + Row row4 = table.children[3] + + then: + row1.children.size() == 3 + + and: + row2.children.size() == 3 + row1.children[0] == row2.children[0] + + and: + row3.children.size() == 3 + row1.children[0] == row3.children[0] + + and: + row4.children.size() == 3 + row1.children[0] != row4.children[0] + } + + def "column widths are set correctly when rowspan is set"() { + when: + Document result = builder.create { + document { + table(width:400, border:[size:0], columns:[1, 3]) { + row { + cell(rowspan:3) { + text 'COL1-1' + } + cell('COL1-2') + } + row { + cell('COL2-1') + } + row { + cell('COL3-1') + } + row { + cell('COL4-1') + cell('COL4-2') + } + } + } + }.document + + Table table = result.children[0] + + then: + table.children.each { + assert it.children[0].width == 100 + assert it.children[1].width == 300 + } + } + +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TestBuilder.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TestBuilder.groovy new file mode 100644 index 0000000..af75c8b --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/TestBuilder.groovy @@ -0,0 +1,15 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import groovy.transform.InheritConstructors +import org.xbib.graphics.pdfbox.groovy.Document +import org.xbib.graphics.pdfbox.groovy.builder.DocumentBuilder + +@InheritConstructors +class TestBuilder extends DocumentBuilder { + + @Override + void initializeDocument(Document document) { } + + @Override + void writeDocument(Document document) { } +} diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/UnitUtilSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/UnitUtilSpec.groovy new file mode 100644 index 0000000..cd9bc72 --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/UnitUtilSpec.groovy @@ -0,0 +1,135 @@ +package org.xbib.graphics.pdfbox.groovy.test + +import org.xbib.graphics.pdfbox.groovy.UnitUtil +import spock.lang.Specification +import spock.lang.Unroll + +class UnitUtilSpec extends Specification { + + @Unroll + def "convert point to mm"() { + expect: + UnitUtil.pointToMm(point) == result + + where: + point | result + 0 | 0 + 72 | 25.4 + 108 | 38.1 + } + + @Unroll + def "convert point to cm"() { + expect: + UnitUtil.pointToCm(point) == result + + where: + point | result + 0 | 0 + 72 | 2.54 + 108 | 3.81 + } + + @Unroll + def "convert point to inch"() { + expect: + UnitUtil.pointToInch(point) == result + + where: + point | result + 0 | 0 + 72 | 1 + 108 | 1.5 + } + + @Unroll + def "convert inch to point"() { + expect: + UnitUtil.inchToPoint(inch) == result + + where: + inch | result + 0 | 0 + 1 | 72 + 1.5 | 108 + } + + @Unroll + def "convert point to twip"() { + expect: + UnitUtil.pointToTwip(point) == result + + where: + point | result + 0 | 0 + 1 | 20 + 1.5 | 30 + 2 | 40 + } + + @Unroll + def "convert twip to point"() { + expect: + UnitUtil.twipToPoint(twip) == result + + where: + twip | result + 0 | 0 + 20 | 1 + 30 | 1.5 + 40 | 2 + } + + @Unroll + def "convert point to pica"() { + expect: + UnitUtil.pointToPica(point) == result + + where: + point | result + 0 | 0 + 1 | 6 + 1.5 | 9 + 2 | 12 + } + + @Unroll + def "convert pica to point"() { + expect: + UnitUtil.picaToPoint(pica) == result + + where: + pica | result + 0 | 0 + 6 | 1 + 9 | 1.5 + 12 | 2 + } + + @Unroll + def "convert point to eight point"() { + expect: + UnitUtil.pointToEigthPoint(point) == result + + where: + point | result + 0 | 0 + 1 | 8 + 1.5 | 12 + 2 | 16 + } + + @Unroll + def "convert eight point to point"() { + expect: + UnitUtil.eightPointToPoint(eightPoint) == result + + where: + eightPoint | result + 0 | 0 + 8 | 1 + 12 | 1.5 + 16 | 2 + } + +} diff --git a/graphics-pdfbox-groovy/src/test/resources/cat.jpg b/graphics-pdfbox-groovy/src/test/resources/cat.jpg new file mode 100644 index 0000000..7ac7580 Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/cat.jpg differ diff --git a/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSans-Bold.ttf b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSans-Bold.ttf new file mode 100644 index 0000000..ab4cdee Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSans-Bold.ttf differ diff --git a/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSans-Regular.ttf b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000..ebd7703 Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSans-Regular.ttf differ diff --git a/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKjp-Regular.ttf b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKjp-Regular.ttf new file mode 100644 index 0000000..bb71b63 Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKjp-Regular.ttf differ diff --git a/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKsc-Regular.ttf b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKsc-Regular.ttf new file mode 100644 index 0000000..1724921 Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKsc-Regular.ttf differ diff --git a/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKtc-Bold.ttf b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKtc-Bold.ttf new file mode 100644 index 0000000..cc8729b Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKtc-Bold.ttf differ diff --git a/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKtc-Regular.ttf b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKtc-Regular.ttf new file mode 100644 index 0000000..0e76702 Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/fonts/NotoSansCJKtc-Regular.ttf differ diff --git a/graphics-pdfbox-groovy/src/test/resources/ghost.pdf b/graphics-pdfbox-groovy/src/test/resources/ghost.pdf new file mode 100644 index 0000000..b63e1c8 Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/ghost.pdf differ diff --git a/graphics-pdfbox-groovy/src/test/resources/img/logo-print.png b/graphics-pdfbox-groovy/src/test/resources/img/logo-print.png new file mode 100644 index 0000000..a34a6f3 Binary files /dev/null and b/graphics-pdfbox-groovy/src/test/resources/img/logo-print.png differ diff --git a/graphics-pdfbox-groovy/src/test/resources/log4j2-test.xml b/graphics-pdfbox-groovy/src/test/resources/log4j2-test.xml new file mode 100644 index 0000000..1258d7f --- /dev/null +++ b/graphics-pdfbox-groovy/src/test/resources/log4j2-test.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 32b6cbc..f08e1a8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,8 +3,9 @@ include 'graphics-vector' include 'graphics-vector-eps' include 'graphics-vector-pdf' include 'graphics-vector-svg' -include 'graphics-pdfbox' -include 'graphics-pdfbox-layout' include 'graphics-chart' include 'graphics-barcode' include 'graphics-ghostscript' +include 'graphics-pdfbox' +include 'graphics-pdfbox-layout' +include 'graphics-pdfbox-groovy'