PDFScript is an open source software library for script based PDF generation using a rendering evaluation graph. The graph nodes are used to evaluate the bounding boxes of each renderable before it gets rendered. The evaluation graphs enables PDFScript to auto-adjust the renderables within the boundaries of a page.
Available in jcenter. Can be included like compile 'one.leftshift.pdfscript:pdfscript:0.25.0'
.
A PDF script is initiated by calling one of the static methods e.g. dinA4. The static method call opens a new PDFScriptStream, which is used to create an evaluation graph. By calling execute on the script stream, the PDF pages gets rendered and the PDF is returned as a byte array.
dinA4 { text("Hello World") }.execute()
A PDFScriptStream automatically takes care of the boundaries of a PDF page format (e.g. dinA4). If a row or column overflows the available space, the text automatically wraps to the available space.
For example the following script creates an evaluation graph which leads to multiple lines.
dinA4 {
text("Lorem ipsum dolor sit amet, consectetur adipisici elit, sed eiusmod tempor incidunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquid ex ea commodi consequat. Quis aute iure reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint obcaecat cupiditat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")
}
By using the withHeader and withFooter function, it is possible to create a header and/or footer content for each page. PDFScript automatically takes care of the page format boundary adjustments.
dinA4 {
withHeader {
text("Header Text")
}
withFooter {
text("Footer Text")
}
}
PDFScript provides a flexible way of table rendering. By default, PDFScript auto adjusts each column by giving each column the same amount of space.
dinA4 {
table {
row {
col { text("Column 1") }
col { text("Column 2") }
}
row {
col { text("Column 3") }
col { text("Column 4") }
}
}
}
In order to overwrite the default behaviour, it is possible to set the ratio of each column at the row level.
A ratio setting e.g. ratio(40, 60)
means, that the first column has a width of 40% while the second column
has a width of 60%.
dinA4 {
table {
row({ratio(40, 60)}) {
col { text("Column 1") }
col { text("Column 2") }
}
row({ratio(60, 40)}) {
col { text("Column 3") }
col { text("Column 4") }
}
}
}
PDFScript renders an jpg or png image by calling image with an image source, width and height argument. The image source can be a url, a byte array or a supplier of an input stream. The supplier handling is necessary because of the possibility of multiple image render executions. For example an image within a header or footer is rendered for each page.
image("https://example.com/image.jpg", 150, 100)
image(getByteArray(), 150, 100)
image({getInputStream()}, 150, 100)
Analogous to the image call the svg method lets define a svg renderable. The given svg image gets encoded to a png image on the fly.
svg("https://example.com/image.svg", 150, 100)
svg(getByteArray(), 150, 100)
svg({getInputStream()}, 150, 100)
By calling the tab renderable a tabulator gets rendered. By default, the tabulator space is set to 100 points. The tabulator space value can be set by specifying the amount at the tab call.
text("A")
tab()
text("B")
tab(200)
Subscript and superscript renderables are elements that are set slightly below or above the normal line of text. They are smaller than the standard text and appear either below the baseline (subscript) or above the baseline (superscript).
superscript("A")
text("B")
subscript("A")
A paragraph groups multiple renderables and separates them from other elements by a newline before and after the paragraph.
paragraph {
text("A")
text("B")
text("C")
}
paragraph {
text("D")
text("E")
text("F")
}
By using the bold renderable a text can be styled in a bold manner. The specific font is selected automatically. When using a non standard font the bold style font have to be registered.
val font1 = loadFont(document, "/SpecialFont-Regular.ttf")
val font2 = loadFont(document, "/SpecialFont-Bold.ttf")
val fontProvider = FontProvider()
fontProvider.addFont(font1)
fontProvider.addFont(font2)
dinA4({ font(font1) }, fontProvider) {
text("A")
bold("B")
text("C")
}
A canvas supports to draw elements on an absolute position without adjusting the current position of the element flow. So by the use of a canvas it is possible to draw elements in a free way onto the pdf.
Each function within the withCanvas block accepts the x and y coordinates on the pdf. While the x coordinate starts on the left of the page, the y coordinate starts on the bottom. A negative x value can be used to start on the right and a negative y value can be used to start from the top.
dinA4 {
withCanvas {
drawCircle(0, -100, 5)
drawLine(0, -200, 10, -200)
drawRect(0, -300, 10, 10)
drawSvg("/image.svg", -400, 0, 0.25)
drawImage("/image.jpg", 100, 200, -400, -200)
drawText("text", -500, 0, 0.25)
}
}
Within a text renderable it is possible to render the current page number as well as the total amount of pages. To do this, simple add either the {{page}} and/or {pages}} expression within the text string. Alternatively the variables can be added by the expressions #PAGE and #PAGES.
text("Page {{page}} of {{pages}}")
text("Page #PAGE of #PAGES")
PDFScript allows the styling of each renderable by applying a styler function as the first parameter.
paragraph({paddingTop(5); paddingBottom(5)}) {
text("B")
}
name | description | values |
---|---|---|
font | the font to use | font qualifier |
fontName | the name of the font to use | font qualifier |
fontSize | the size of the font to use | number |
foreground | the color which is used to render text and line elements | hex code or color qualifier |
background | the color which is used to render element backgrounds | hex code or color qualifier |
border | determines if and how the borders should be rendered | "none", hex code or color qualifier |
borderBottom | determines if and how the bottom border should be rendered | "none", hex code or color qualifier |
borderTop | determines if and how the top border should be rendered | "none", hex code or color qualifier |
borderLeft | determines if and how the left border should be rendered | "none", hex code or color qualifier |
borderRight | determines if and how the right border should be rendered | "none", hex code or color qualifier |
paddingTop | determines the top padding | number |
paddingBottom | determines the bottom padding | number |
align | indicates the text alignment | "left", "center", "right" |
ratio | the ratio of the row columns | number varargs |
PDFScript offers a convenient way to embed a font in a pdf document.
private fun loadFont(document: PDDocument, path:String): PDFont {
val fontStream = TextTest::class.java.getResourceAsStream(path)
return PDType0Font.load(document, fontStream)
}
val document = PDDocument()
val font = loadFont(document, "/customFont.ttf")
val interceptor = RawCommandsInterceptor()
dinA4({ font(font) }) { text("č") }.execute(interceptor, document)
Sometimes it is necessary to have a fallback font, if glyphs were used which are not supported by the current font. For this case, a PDFontResolver can be used in order to build a font chain.
private fun loadFont(document: PDDocument, path:String): PDFont {
val fontStream = TextTest::class.java.getResourceAsStream(path)
return PDType0Font.load(document, fontStream)
}
val document = PDDocument()
val font1 = loadFont(document, "/customFont1.ttf")
val font2 = loadFont(document, "/customFont2.ttf")
val interceptor = RawCommandsInterceptor()
dinA4({ font(PDFontResolver(font1, font2)) }) { text("\u0627") }.execute(interceptor, document)
The PDFScript execute method accepts an interceptor instance which can be used to hook into the events of the PDF generation.
val interceptor = RawCommandsInterceptor()
dinA4 { text("Hello World") }.execute(interceptor)
print(interceptor.commands)
PDFScript supports pixel perfect PDF rendering unit tests by using the RawCommandsInterceptor. The RawCommandsInterceptor collects the raw PDF instructions so that a unit test simple asserts the actual raw commands with the expected commands. (collected from a previous pdf rendering run)
@Test
fun `create a superscript text`() {
val interceptor = RawCommandsInterceptor()
dinA4 {
text("text")
superscript("superscript")
}.execute(interceptor)
val expected = listOf(
"setFont[Helvetica, 12.0]",
"beginText:[]",
"newLineAtOffset:[70.86614, 758.7796]",
"showText:[text ]",
"endText:[]",
"setFont[Helvetica, 7.2000003]",
"beginText:[]",
"newLineAtOffset:[93.54614, 764.2796]",
"showText:[superscript]",
"endText:[]",
"setFont[Helvetica, 12.0]"
)
assertEquals(expected, interceptor.commands)
}
PDFScript is supporting the native handling of PDF elements by using one of the native reader/writer classes. The PdfTextReader for example extracts all text elements together with the corresponding bounding boxes from a pdf file while the PdfTextWriter creates a new pdf file by a list of pdfText bounding boxes.
val elements = PdfTextReader().read("/pdf/result.pdf")
val pdfBytes = PdfTextWriter().write(elements)
Releases are triggered locally. Just a tag will be pushed and CI pipelines take care of the rest.
Run ./gradlew final -x sendReleaseEmail -Prelease.scope=major
locally.
Run ./gradlew final -x sendReleaseEmail -Prelease.scope=minor
locally.
Run ./gradlew final -x sendReleaseEmail -Prelease.scope=patch
locally.
- Footnotes support
- Endnotes support