add Groovy PDF/PDFBox extensions
This commit is contained in:
parent
d00c4de210
commit
b4bee6e957
99 changed files with 5510 additions and 5 deletions
|
@ -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
|
||||
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
|
||||
|
|
34
gradle/compile/groovy.gradle
Normal file
34
gradle/compile/groovy.gradle
Normal file
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
202
graphics-pdfbox-groovy/LICENSE.txt
Normal file
202
graphics-pdfbox-groovy/LICENSE.txt
Normal file
|
@ -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.
|
8
graphics-pdfbox-groovy/NOTICE.txt
Normal file
8
graphics-pdfbox-groovy/NOTICE.txt
Normal file
|
@ -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.
|
52
graphics-pdfbox-groovy/build.gradle
Normal file
52
graphics-pdfbox-groovy/build.gradle
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
enum BarcodeType {
|
||||
CODE39
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
class BaseNode {
|
||||
|
||||
def element
|
||||
|
||||
def parent
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
trait Bookmarkable {
|
||||
String ref
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
trait ColorAssignable {
|
||||
Color color = new Color()
|
||||
|
||||
void setColor(String value) {
|
||||
color.color = value
|
||||
}
|
||||
}
|
|
@ -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<Map> 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
class HeaderFooterOptions {
|
||||
|
||||
Date dateGenerated
|
||||
|
||||
String pageCount
|
||||
|
||||
String pageNumber
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
class LineBreak extends BaseNode {
|
||||
Integer height = 0
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
class Link extends Text {
|
||||
String url
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
trait Linkable {
|
||||
String url
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
class PageBreak extends BaseNode {
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
class Row extends BaseNode implements Stylable, BackgroundAssignable {
|
||||
|
||||
List<Cell> children = []
|
||||
|
||||
Integer width
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
trait Stylable {
|
||||
|
||||
Font font
|
||||
|
||||
String style
|
||||
}
|
|
@ -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<Row> children = []
|
||||
|
||||
Integer padding = 10
|
||||
|
||||
Integer width = 0
|
||||
|
||||
List<BigDecimal> 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<BigDecimal> 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<BigDecimal> computeColumnWidths() {
|
||||
|
||||
BigDecimal relativeTotal = columns.sum() as BigDecimal
|
||||
|
||||
BigDecimal totalBorderWidth = (columnCount + 1) * border.size
|
||||
|
||||
BigDecimal totalCellWidth = width - totalBorderWidth
|
||||
|
||||
List<BigDecimal> 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.xbib.graphics.pdfbox.groovy
|
||||
|
||||
class Text extends BaseNode implements Stylable, Linkable, Bookmarkable {
|
||||
String value
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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<COSStream> 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<Object, Object> 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 {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Map> fontDefs
|
||||
|
||||
RenderState renderState = RenderState.PAGE
|
||||
|
||||
Document document
|
||||
|
||||
protected List<String> 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())
|
||||
}
|
||||
}
|
|
@ -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<PDPage> 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()
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package org.xbib.graphics.pdfbox.groovy.builder
|
||||
|
||||
enum RenderState {
|
||||
PAGE, HEADER, FOOTER, CUSTOM
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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 }
|
||||
|
||||
}
|
|
@ -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 }
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 }
|
||||
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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<Renderable> 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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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<Renderable> 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
|
||||
}
|
||||
}
|
|
@ -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<ParagraphLine> 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<ParagraphLine> 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
|
||||
}
|
||||
}
|
|
@ -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<ParagraphLine> 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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<CellRenderer> 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()
|
||||
}
|
||||
}
|
|
@ -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<RowRenderer> 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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.xbib.graphics.pdfbox.groovy.render.element
|
||||
|
||||
import org.xbib.graphics.pdfbox.groovy.Barcode
|
||||
|
||||
class BarcodeElement {
|
||||
|
||||
Barcode node
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.xbib.graphics.pdfbox.groovy.render.element
|
||||
|
||||
import org.xbib.graphics.pdfbox.groovy.Image
|
||||
|
||||
class ImageElement {
|
||||
|
||||
Image node
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.xbib.graphics.pdfbox.groovy.render.element
|
||||
|
||||
import org.xbib.graphics.pdfbox.groovy.Line
|
||||
|
||||
class LineElement {
|
||||
Line node
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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}" : ''}"
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<String, TrueTypeFont> ttf = new HashMap<>()
|
||||
private final Map<String, TrueTypeFont> 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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<RowRenderer> 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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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) { }
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
BIN
graphics-pdfbox-groovy/src/test/resources/cat.jpg
Normal file
BIN
graphics-pdfbox-groovy/src/test/resources/cat.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 67 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
graphics-pdfbox-groovy/src/test/resources/ghost.pdf
Normal file
BIN
graphics-pdfbox-groovy/src/test/resources/ghost.pdf
Normal file
Binary file not shown.
BIN
graphics-pdfbox-groovy/src/test/resources/img/logo-print.png
Normal file
BIN
graphics-pdfbox-groovy/src/test/resources/img/logo-print.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 673 B |
13
graphics-pdfbox-groovy/src/test/resources/log4j2-test.xml
Normal file
13
graphics-pdfbox-groovy/src/test/resources/log4j2-test.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration status="OFF">
|
||||
<appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="[%d{ISO8601}][%-5p][%-25c][%t] %m%n"/>
|
||||
</Console>
|
||||
</appenders>
|
||||
<Loggers>
|
||||
<Root level="debug">
|
||||
<AppenderRef ref="Console" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</configuration>
|
|
@ -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'
|
||||
|
|
Loading…
Reference in a new issue