add Groovy PDF/PDFBox extensions

This commit is contained in:
Jörg Prante 2021-02-25 17:01:47 +01:00
parent d00c4de210
commit b4bee6e957
99 changed files with 5510 additions and 5 deletions

View file

@ -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

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

View file

@ -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

View file

@ -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,

View 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.

View 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.

View 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
}

View file

@ -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
}
}

View file

@ -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())
}
}

View file

@ -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
}
}
}

View file

@ -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())
}
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.groovy
enum BarcodeType {
CODE39
}

View file

@ -0,0 +1,8 @@
package org.xbib.graphics.pdfbox.groovy
class BaseNode {
def element
def parent
}

View file

@ -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()
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.groovy
trait Bookmarkable {
String ref
}

View file

@ -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 }
}
}

View file

@ -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
}

View file

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

View file

@ -0,0 +1,9 @@
package org.xbib.graphics.pdfbox.groovy
trait ColorAssignable {
Color color = new Color()
void setColor(String value) {
color.color = value
}
}

View file

@ -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()
}
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,10 @@
package org.xbib.graphics.pdfbox.groovy
class HeaderFooterOptions {
Date dateGenerated
String pageCount
String pageNumber
}

View file

@ -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
}

View file

@ -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())
}
}

View file

@ -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
}
}

View file

@ -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
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.groovy
class LineBreak extends BaseNode {
Integer height = 0
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.groovy
class Link extends Text {
String url
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.groovy
trait Linkable {
String url
}

View file

@ -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 }
}
}

View file

@ -0,0 +1,4 @@
package org.xbib.graphics.pdfbox.groovy
class PageBreak extends BaseNode {
}

View file

@ -0,0 +1,8 @@
package org.xbib.graphics.pdfbox.groovy
class Row extends BaseNode implements Stylable, BackgroundAssignable {
List<Cell> children = []
Integer width
}

View file

@ -0,0 +1,8 @@
package org.xbib.graphics.pdfbox.groovy
trait Stylable {
Font font
String style
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.groovy
class Text extends BaseNode implements Stylable, Linkable, Bookmarkable {
String value
}

View file

@ -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
}
}

View file

@ -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 }
}

View file

@ -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
}
}

View file

@ -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 {
}
}
}

View file

@ -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())
}
}

View file

@ -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()
}
}

View file

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

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.groovy.builder
enum RenderState {
PAGE, HEADER, FOOTER, CUSTOM
}

View file

@ -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
}
}

View file

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

View file

@ -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
}
}

View file

@ -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 }
}

View file

@ -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 }
}

View file

@ -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
}
}

View file

@ -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 }
}

View file

@ -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 }
}

View file

@ -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 }
}

View file

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

View file

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

View file

@ -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 }
}

View file

@ -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 }
}

View file

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

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -0,0 +1,8 @@
package org.xbib.graphics.pdfbox.groovy.render.element
import org.xbib.graphics.pdfbox.groovy.Barcode
class BarcodeElement {
Barcode node
}

View file

@ -0,0 +1,8 @@
package org.xbib.graphics.pdfbox.groovy.render.element
import org.xbib.graphics.pdfbox.groovy.Image
class ImageElement {
Image node
}

View file

@ -0,0 +1,7 @@
package org.xbib.graphics.pdfbox.groovy.render.element
import org.xbib.graphics.pdfbox.groovy.Line
class LineElement {
Line node
}

View file

@ -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
}

View file

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

View file

@ -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}" : ''}"
}
}

View file

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

View file

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

View file

@ -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
}
}

View file

@ -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()
}
}
}
}

View file

@ -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
}
}

View file

@ -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'
}
}

View file

@ -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
}
}

View file

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

View file

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

View file

@ -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
}
}

View file

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

View file

@ -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
}
}

View file

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

View file

@ -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
}
}
}

View file

@ -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) { }
}

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

View 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>

View file

@ -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'