adding table, boxable, better font descriptor with TTF fonts

This commit is contained in:
Jörg Prante 2021-03-02 14:24:31 +01:00
parent d434a9e4ee
commit 4ad07247b2
109 changed files with 8698 additions and 1076 deletions

View file

@ -228,7 +228,7 @@ abstract class BaseBuilderSpec extends Specification {
builder.create {
document {
paragraph {
barcode(height: 150.px, width: 500.px, value: '12345678')
barcode(height: 150.px, width: 500.px, value: '12345678', type: 'CODE39')
}
}
}

View file

@ -1,12 +1,16 @@
module org.xbib.graphics.layout.pdfbox {
exports org.xbib.graphics.pdfbox.layout.boxable;
exports org.xbib.graphics.pdfbox.layout.elements;
exports org.xbib.graphics.pdfbox.layout.elements.render;
exports org.xbib.graphics.pdfbox.layout.font;
exports org.xbib.graphics.pdfbox.layout.shape;
exports org.xbib.graphics.pdfbox.layout.table;
exports org.xbib.graphics.pdfbox.layout.table.render;
exports org.xbib.graphics.pdfbox.layout.text;
exports org.xbib.graphics.pdfbox.layout.text.annotations;
exports org.xbib.graphics.pdfbox.layout.util;
requires transitive org.xbib.graphics.barcode;
requires transitive org.xbib.graphics.pdfbox;
requires transitive java.desktop;
requires java.logging;
}

View file

@ -0,0 +1,26 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import java.io.IOException;
public class BaseTable extends Table<PDPage> {
public BaseTable(float yStart, float yStartNewPage, float bottomMargin, float width, float margin, PDDocument document, PDPage currentPage, boolean drawLines, boolean drawContent) throws IOException {
super(yStart, yStartNewPage, 0, bottomMargin, width, margin, document, currentPage, drawLines, drawContent, new DefaultPageProvider(document, currentPage.getMediaBox()));
}
public BaseTable(float yStart, float yStartNewPage, float pageTopMargin, float bottomMargin, float width, float margin, PDDocument document, PDPage currentPage, boolean drawLines, boolean drawContent) throws IOException {
super(yStart, yStartNewPage, pageTopMargin, bottomMargin, width, margin, document, currentPage, drawLines, drawContent, new DefaultPageProvider(document, currentPage.getMediaBox()));
}
public BaseTable(float yStart, float yStartNewPage, float pageTopMargin, float bottomMargin, float width, float margin, PDDocument document, PDPage currentPage, boolean drawLines, boolean drawContent, final PageProvider<PDPage> pageProvider) throws IOException {
super(yStart, yStartNewPage, pageTopMargin, bottomMargin, width, margin, document, currentPage, drawLines, drawContent, pageProvider);
}
@Override
protected void loadFonts() {
// Do nothing as we don't have any fonts to load
}
}

View file

@ -0,0 +1,707 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import java.awt.Color;
import java.io.IOException;
public class Cell<T extends PDPage> {
private float width;
private Float height;
private String text;
private PDFont font = PDType1Font.HELVETICA;
private PDFont fontBold = PDType1Font.HELVETICA_BOLD;
private float fontSize = 8;
private Color fillColor;
private Color textColor = Color.BLACK;
private final Row<T> row;
private WrappingFunction wrappingFunction;
private boolean isHeaderCell = false;
private boolean isColspanCell = false;
// default padding
private float leftPadding = 5f;
private float rightPadding = 5f;
private float topPadding = 5f;
private float bottomPadding = 5f;
// default border
private LineStyle leftBorderStyle = new LineStyle(Color.BLACK, 1);
private LineStyle rightBorderStyle = new LineStyle(Color.BLACK, 1);
private LineStyle topBorderStyle = new LineStyle(Color.BLACK, 1);
private LineStyle bottomBorderStyle = new LineStyle(Color.BLACK, 1);
private Paragraph paragraph = null;
private float lineSpacing = 1;
private boolean textRotated = false;
private HorizontalAlignment align;
private VerticalAlignment valign;
/**
* <p>
* Constructs a cell with the default alignment
* {@link VerticalAlignment#TOP} {@link HorizontalAlignment#LEFT}.
* </p>
*
* @param row
* @param width
* @param text
* @param isCalculated
* @see Cell#Cell(Row, float, String, boolean, HorizontalAlignment,
* VerticalAlignment)
*/
Cell(Row<T> row, float width, String text, boolean isCalculated) {
this(row, width, text, isCalculated, HorizontalAlignment.LEFT, VerticalAlignment.TOP);
}
/**
* Constructs a cell.
*
* @param row The parent row
* @param width absolute width in points or in % of table width (depending on
* the parameter {@code isCalculated})
* @param text The text content of the cell
* @param isCalculated If {@code true}, the width is interpreted in % to the table
* width
* @param align The {@link HorizontalAlignment} of the cell content
* @param valign The {@link VerticalAlignment} of the cell content
* @see Cell#Cell(Row, float, String, boolean)
*/
Cell(Row<T> row, float width, String text, boolean isCalculated, HorizontalAlignment align,
VerticalAlignment valign) {
this.row = row;
if (isCalculated) {
double calclulatedWidth = ((row.getWidth() * width) / 100);
this.width = (float) calclulatedWidth;
} else {
this.width = width;
}
if (getWidth() > row.getWidth()) {
throw new IllegalArgumentException(
"Cell Width=" + getWidth() + " can't be bigger than row width=" + row.getWidth());
}
//check if we have new default font
if (!FontUtils.getDefaultfonts().isEmpty()) {
font = FontUtils.getDefaultfonts().get("font");
fontBold = FontUtils.getDefaultfonts().get("fontBold");
}
this.text = text == null ? "" : text;
this.align = align;
this.valign = valign;
this.wrappingFunction = null;
}
/**
* Retrieves cell's text {@link Color}. Default color is black.
*
* @return {@link Color} of the cell's text
*/
public Color getTextColor() {
return textColor;
}
/**
* <p>
* Sets cell's text {@link Color}.
* </p>
*
* @param textColor designated text {@link Color}
*/
public void setTextColor(Color textColor) {
this.textColor = textColor;
}
/**
* <p>
* Gets fill (background) {@link Color} for the current cell.
* </p>
*
* @return Fill {@link Color} for the cell
*/
public Color getFillColor() {
return fillColor;
}
/**
* <p>
* Sets fill (background) {@link Color} for the current cell.
* </p>
*
* @param fillColor Fill {@link Color} for the cell
*/
public void setFillColor(Color fillColor) {
this.fillColor = fillColor;
}
/**
* <p>
* Gets cell's width.
* </p>
*
* @return Cell's width
*/
public float getWidth() {
return width;
}
/**
* <p>
* Gets cell's width without (left,right) padding.
*
* @return Inner cell's width
*/
public float getInnerWidth() {
return getWidth() - getLeftPadding() - getRightPadding()
- (leftBorderStyle == null ? 0 : leftBorderStyle.getWidth())
- (rightBorderStyle == null ? 0 : rightBorderStyle.getWidth());
}
/**
* <p>
* Gets cell's height without (top,bottom) padding.
*
* @return Inner cell's height
*/
public float getInnerHeight() {
return getHeight() - getBottomPadding() - getTopPadding()
- (topBorderStyle == null ? 0 : topBorderStyle.getWidth())
- (bottomBorderStyle == null ? 0 : bottomBorderStyle.getWidth());
}
/**
* <p>
* Retrieves text from current cell
* </p>
*
* @return cell's text
*/
public String getText() {
return text;
}
/**
* <p>
* Sets cell's text value
* </p>
*
* @param text Text value of the cell
*/
public void setText(String text) {
this.text = text;
// paragraph invalidated
paragraph = null;
}
/**
* <p>
* Gets appropriate {@link PDFont} for current cell.
* </p>
*
* @return {@link PDFont} for current cell
* @throws IllegalArgumentException if <code>font</code> is not set.
*/
public PDFont getFont() {
if (font == null) {
throw new IllegalArgumentException("Font not set.");
}
if (isHeaderCell) {
return fontBold;
} else {
return font;
}
}
/**
* <p>
* Sets appropriate {@link PDFont} for current cell.
* </p>
*
* @param font {@link PDFont} for current cell
*/
public void setFont(PDFont font) {
this.font = font;
// paragraph invalidated
paragraph = null;
}
/**
* <p>
* Gets {@link PDFont} size for current cell (in points).
* </p>
*
* @return {@link PDFont} size for current cell (in points).
*/
public float getFontSize() {
return fontSize;
}
/**
* <p>
* Sets {@link PDFont} size for current cell (in points).
* </p>
*
* @param fontSize {@link PDFont} size for current cell (in points).
*/
public void setFontSize(float fontSize) {
this.fontSize = fontSize;
// paragraph invalidated
paragraph = null;
}
/**
* <p>
* Retrieves a valid {@link Paragraph} depending of cell's {@link PDFont}
* and value rotation.
* </p>
*
* <p>
* If cell has rotated value then {@link Paragraph} width is depending of
* {@link Cell#getInnerHeight()} otherwise {@link Cell#getInnerWidth()}
* </p>
*
* @return Cell's {@link Paragraph}
*/
public Paragraph getParagraph() {
if (paragraph == null) {
// if it is header cell then use font bold
if (isHeaderCell) {
if (isTextRotated()) {
paragraph = new Paragraph(text, fontBold, fontSize, getInnerHeight(), align, textColor, null,
wrappingFunction, lineSpacing);
} else {
paragraph = new Paragraph(text, fontBold, fontSize, getInnerWidth(), align, textColor, null,
wrappingFunction, lineSpacing);
}
} else {
if (isTextRotated()) {
paragraph = new Paragraph(text, font, fontSize, getInnerHeight(), align, textColor, null,
wrappingFunction, lineSpacing);
} else {
paragraph = new Paragraph(text, font, fontSize, getInnerWidth(), align, textColor, null,
wrappingFunction, lineSpacing);
}
}
}
return paragraph;
}
public float getExtraWidth() {
return this.row.getLastCellExtraWidth() + getWidth();
}
/**
* <p>
* Gets the cell's height according to {@link Row}'s height
* </p>
*
* @return {@link Row}'s height
*/
public float getHeight() {
return row.getHeight();
}
/**
* <p>
* Gets the height of the single cell, opposed to {@link #getHeight()},
* which returns the row's height.
* </p>
* <p>
* Depending of rotated/normal cell's value there is two cases for
* calculation:
* </p>
* <ol>
* <li>Rotated value - cell's height is equal to overall text length in the
* cell with necessery paddings (top,bottom)</li>
* <li>Normal value - cell's height is equal to {@link Paragraph}'s height
* with necessery paddings (top,bottom)</li>
* </ol>
*
* @return Cell's height
* @throws IllegalStateException if <code>font</code> is not set.
*/
public float getCellHeight() {
if (height != null) {
return height;
}
if (isTextRotated()) {
try {
// TODO: maybe find more optimal way then this
return getFont().getStringWidth(getText()) / 1000 * getFontSize() + getTopPadding()
+ (getTopBorder() == null ? 0 : getTopBorder().getWidth()) + getBottomPadding()
+ (getBottomBorder() == null ? 0 : getBottomBorder().getWidth());
} catch (final IOException e) {
throw new IllegalStateException("Font not set.", e);
}
} else {
return getTextHeight() + getTopPadding() + getBottomPadding()
+ (getTopBorder() == null ? 0 : getTopBorder().getWidth())
+ (getBottomBorder() == null ? 0 : getBottomBorder().getWidth());
}
}
/**
* <p>
* Sets the height of the single cell.
* </p>
*
* @param height Cell's height
*/
public void setHeight(final Float height) {
this.height = height;
}
/**
* <p>
* Gets {@link Paragraph}'s height
* </p>
*
* @return {@link Paragraph}'s height
*/
public float getTextHeight() {
return getParagraph().getHeight();
}
/**
* <p>
* Gets {@link Paragraph}'s width
* </p>
*
* @return {@link Paragraph}'s width
*/
public float getTextWidth() {
return getParagraph().getWidth();
}
/**
* <p>
* Gets cell's left padding (in points).
* </p>
*
* @return Cell's left padding (in points).
*/
public float getLeftPadding() {
return leftPadding;
}
/**
* <p>
* Sets cell's left padding (in points)
* </p>
*
* @param cellLeftPadding Cell's left padding (in points).
*/
public void setLeftPadding(float cellLeftPadding) {
this.leftPadding = cellLeftPadding;
// paragraph invalidated
paragraph = null;
}
/**
* <p>
* Gets cell's right padding (in points).
* </p>
*
* @return Cell's right padding (in points).
*/
public float getRightPadding() {
return rightPadding;
}
/**
* <p>
* Sets cell's right padding (in points)
* </p>
*
* @param cellRightPadding Cell's right padding (in points).
*/
public void setRightPadding(float cellRightPadding) {
this.rightPadding = cellRightPadding;
// paragraph invalidated
paragraph = null;
}
/**
* <p>
* Gets cell's top padding (in points).
* </p>
*
* @return Cell's top padding (in points).
*/
public float getTopPadding() {
return topPadding;
}
/**
* <p>
* Sets cell's top padding (in points)
* </p>
*
* @param cellTopPadding Cell's top padding (in points).
*/
public void setTopPadding(float cellTopPadding) {
this.topPadding = cellTopPadding;
}
/**
* <p>
* Gets cell's bottom padding (in points).
* </p>
*
* @return Cell's bottom padding (in points).
*/
public float getBottomPadding() {
return bottomPadding;
}
/**
* <p>
* Sets cell's bottom padding (in points)
* </p>
*
* @param cellBottomPadding Cell's bottom padding (in points).
*/
public void setBottomPadding(float cellBottomPadding) {
this.bottomPadding = cellBottomPadding;
}
/**
* <p>
* Gets free vertical space of cell.
* </p>
*
* <p>
* If cell has rotated value then free vertical space is equal inner cell's
* height ({@link #getInnerHeight()}) subtracted to the longest line of
* rotated {@link Paragraph} otherwise it's just cell's inner height (
* {@link #getInnerHeight()}) subtracted with width of the normal
* {@link Paragraph}.
* </p>
*
* @return Free vertical space of the cell's.
*/
public float getVerticalFreeSpace() {
if (isTextRotated()) {
// need to calculate max line width so we just iterating through
// lines
for (String line : getParagraph().getLines()) {
}
return getInnerHeight() - getParagraph().getMaxLineWidth();
} else {
return getInnerHeight() - getTextHeight();
}
}
/**
* <p>
* Gets free horizontal space of cell.
* </p>
*
* <p>
* If cell has rotated value then free horizontal space is equal cell's
* inner width ({@link #getInnerWidth()}) subtracted to the
* {@link Paragraph}'s height otherwise it's just cell's
* {@link #getInnerWidth()} subtracted with width of longest line in normal
* {@link Paragraph}.
* </p>
*
* @return Free vertical space of the cell's.
*/
public float getHorizontalFreeSpace() {
if (isTextRotated()) {
return getInnerWidth() - getTextHeight();
} else {
return getInnerWidth() - getParagraph().getMaxLineWidth();
}
}
public HorizontalAlignment getAlign() {
return align;
}
public VerticalAlignment getValign() {
return valign;
}
public boolean isHeaderCell() {
return isHeaderCell;
}
public void setHeaderCell(boolean isHeaderCell) {
this.isHeaderCell = isHeaderCell;
}
public WrappingFunction getWrappingFunction() {
return getParagraph().getWrappingFunction();
}
public void setWrappingFunction(WrappingFunction wrappingFunction) {
this.wrappingFunction = wrappingFunction;
// paragraph invalidated
paragraph = null;
}
public LineStyle getLeftBorder() {
return leftBorderStyle;
}
public LineStyle getRightBorder() {
return rightBorderStyle;
}
public LineStyle getTopBorder() {
return topBorderStyle;
}
public LineStyle getBottomBorder() {
return bottomBorderStyle;
}
public void setLeftBorderStyle(LineStyle leftBorder) {
this.leftBorderStyle = leftBorder;
}
public void setRightBorderStyle(LineStyle rightBorder) {
this.rightBorderStyle = rightBorder;
}
public void setTopBorderStyle(LineStyle topBorder) {
this.topBorderStyle = topBorder;
}
public void setBottomBorderStyle(LineStyle bottomBorder) {
this.bottomBorderStyle = bottomBorder;
}
/**
* <p>
* Easy setting for cell border style.
*
* @param border It is {@link LineStyle} for all borders
* @see LineStyle Rendering line attributes
*/
public void setBorderStyle(LineStyle border) {
this.leftBorderStyle = border;
this.rightBorderStyle = border;
this.topBorderStyle = border;
this.bottomBorderStyle = border;
}
public boolean isTextRotated() {
return textRotated;
}
public void setTextRotated(boolean textRotated) {
this.textRotated = textRotated;
}
public PDFont getFontBold() {
return fontBold;
}
/**
* <p>
* Sets the {@linkplain PDFont font} used for bold text, for example in
* {@linkplain #isHeaderCell() header cells}.
* </p>
*
* @param fontBold The {@linkplain PDFont font} to use for bold text
*/
public void setFontBold(final PDFont fontBold) {
this.fontBold = fontBold;
}
public boolean isColspanCell() {
return isColspanCell;
}
public void setColspanCell(boolean isColspanCell) {
this.isColspanCell = isColspanCell;
}
public void setAlign(HorizontalAlignment align) {
this.align = align;
}
public void setValign(VerticalAlignment valign) {
this.valign = valign;
}
/**
* <p>
* Copies the style of an existing cell to this cell
* </p>
*
* @param sourceCell Source {@link Cell} from which cell style will be copied.
*/
public void copyCellStyle(Cell<?> sourceCell) {
boolean leftBorder = this.leftBorderStyle == null;
setBorderStyle(sourceCell.getTopBorder());
if (leftBorder) {
this.leftBorderStyle = null;// if left border wasn't set, don't set
// it now
}
this.font = sourceCell.getFont();// otherwise paragraph gets invalidated
this.fontBold = sourceCell.getFontBold();
this.fontSize = sourceCell.getFontSize();
setFillColor(sourceCell.getFillColor());
setTextColor(sourceCell.getTextColor());
setAlign(sourceCell.getAlign());
setValign(sourceCell.getValign());
}
/**
* <p>
* Compares the style of a cell with another cell
* </p>
*
* @param sourceCell Source {@link Cell} which will be used for style comparation
* @return boolean if source cell has the same style
*/
public Boolean hasSameStyle(Cell<?> sourceCell) {
if (!sourceCell.getTopBorder().equals(getTopBorder())) {
return false;
}
if (!sourceCell.getFont().equals(getFont())) {
return false;
}
if (!sourceCell.getFontBold().equals(getFontBold())) {
return false;
}
if (!sourceCell.getFillColor().equals(getFillColor())) {
return false;
}
if (!sourceCell.getTextColor().equals(getTextColor())) {
return false;
}
if (!sourceCell.getAlign().equals(getAlign())) {
return false;
}
return sourceCell.getValign().equals(getValign());
}
public void setWidth(float width) {
this.width = width;
}
public float getLineSpacing() {
return lineSpacing;
}
public void setLineSpacing(float lineSpacing) {
this.lineSpacing = lineSpacing;
}
}

View file

@ -0,0 +1,62 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
public class DefaultPageProvider implements PageProvider<PDPage> {
private final PDDocument document;
private final PDRectangle size;
private int currentPageIndex = -1;
public DefaultPageProvider(final PDDocument document, final PDRectangle size) {
this.document = document;
this.size = size;
}
@Override
public PDDocument getDocument() {
return document;
}
@Override
public PDPage createPage() {
currentPageIndex = document.getNumberOfPages();
return getCurrentPage();
}
@Override
public PDPage nextPage() {
if (currentPageIndex == -1) {
currentPageIndex = document.getNumberOfPages();
} else {
currentPageIndex++;
}
return getCurrentPage();
}
@Override
public PDPage previousPage() {
currentPageIndex--;
if (currentPageIndex < 0) {
currentPageIndex = 0;
}
return getCurrentPage();
}
private PDPage getCurrentPage() {
if (currentPageIndex >= document.getNumberOfPages()) {
final PDPage newPage = new PDPage(size);
document.addPage(newPage);
return newPage;
}
return document.getPage(currentPageIndex);
}
}

View file

@ -0,0 +1,28 @@
package org.xbib.graphics.pdfbox.layout.boxable;
class FontMetrics {
private final float ascent;
private final float descent;
private final float height;
public FontMetrics(final float height, final float ascent, final float descent) {
this.height = height;
this.ascent = ascent;
this.descent = descent;
}
public float getAscent() {
return ascent;
}
public float getDescent() {
return descent;
}
public float getHeight() {
return height;
}
}

View file

@ -0,0 +1,133 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.HashMap;
import java.util.Map;
/**
* Utility methods for fonts
*/
public final class FontUtils {
/**
* {@link HashMap} for caching {@link FontMetrics} for designated
* {@link PDFont} because {@link FontUtils#getHeight(PDFont, float)} is
* expensive to calculate and the results are only approximate.
*/
private static final Map<String, FontMetrics> fontMetrics = new HashMap<>();
private static final Map<String, PDFont> defaultFonts = new HashMap<>();
private FontUtils() {
}
/**
* Loads the {@link PDType0Font} to be embedded in the specified
* {@link PDDocument}.
*
* @param document {@link PDDocument} where fonts will be loaded
* @param fontPath font path which will be loaded
* @return The read {@link PDType0Font}
*/
public static PDType0Font loadFont(PDDocument document, String fontPath) {
try {
return PDType0Font.load(document, FontUtils.class.getClassLoader().getResourceAsStream(fontPath));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Retrieving {@link String} width depending on current font size. The width
* of the string in 1/1000 units of text space.
*
* @param font The font of text whose width will be retrieved
* @param text The text whose width will be retrieved
* @param fontSize The font size of text whose width will be retrieved
* @return text width
*/
public static float getStringWidth(final PDFont font, final String text, final float fontSize) {
try {
return font.getStringWidth(text) / 1000 * fontSize;
} catch (final IOException e) {
throw new UncheckedIOException("Unable to determine text width", e);
}
}
/**
* <p>
* Calculate the font ascent distance.
* </p>
*
* @param font The font from which calculation will be applied
* @param fontSize The font size from which calculation will be applied
* @return Positive font ascent distance
*/
public static float getAscent(final PDFont font, final float fontSize) {
final String fontName = font.getName();
if (!fontMetrics.containsKey(fontName)) {
createFontMetrics(font);
}
return fontMetrics.get(fontName).getAscent() * fontSize;
}
/**
* <p>
* Calculate the font descent distance.
* </p>
*
* @param font The font from which calculation will be applied
* @param fontSize The font size from which calculation will be applied
* @return Negative font descent distance
*/
public static float getDescent(final PDFont font, final float fontSize) {
final String fontName = font.getName();
if (!fontMetrics.containsKey(fontName)) {
createFontMetrics(font);
}
return fontMetrics.get(fontName).getDescent() * fontSize;
}
/**
* <p>
* Calculate the font height.
* </p>
*
* @param font {@link PDFont} from which the height will be calculated.
* @param fontSize font size for current {@link PDFont}.
* @return {@link PDFont}'s height
*/
public static float getHeight(final PDFont font, final float fontSize) {
final String fontName = font.getName();
if (!fontMetrics.containsKey(fontName)) {
createFontMetrics(font);
}
return fontMetrics.get(fontName).getHeight() * fontSize;
}
/**
* <p>
* Create basic {@link FontMetrics} for current font.
* <p>
*
* @param font The font from which calculation will be applied <<<<<<< HEAD
*/
private static void createFontMetrics(final PDFont font) {
final float base = font.getFontDescriptor().getXHeight() / 1000;
final float ascent = font.getFontDescriptor().getAscent() / 1000 - base;
final float descent = font.getFontDescriptor().getDescent() / 1000;
fontMetrics.put(font.getName(), new FontMetrics(base + ascent - descent, ascent, descent));
}
public static Map<String, PDFont> getDefaultfonts() {
return defaultFonts;
}
}

View file

@ -0,0 +1,47 @@
package org.xbib.graphics.pdfbox.layout.boxable;
/**
* <p>
* Data container for HTML ordered list elements.
* </p>
*
* @author hstimac
*/
public class HTMLListNode {
/**
* <p>
* Element's current ordering number (e.g third element in the current list)
* </p>
*/
private int orderingNumber;
/**
* <p>
* Element's whole ordering number value (e.g 1.1.2.1)
* </p>
*/
private String value;
public int getOrderingNumber() {
return orderingNumber;
}
public void setOrderingNumber(int orderingNumber) {
this.orderingNumber = orderingNumber;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public HTMLListNode(int orderingNumber, String value) {
this.orderingNumber = orderingNumber;
this.value = value;
}
}

View file

@ -0,0 +1,18 @@
package org.xbib.graphics.pdfbox.layout.boxable;
public enum HorizontalAlignment {
LEFT, CENTER, RIGHT;
public static HorizontalAlignment get(final String key) {
switch (key == null ? "left" : key.toLowerCase().trim()) {
case "left":
return LEFT;
case "center":
return CENTER;
case "right":
return RIGHT;
default:
return LEFT;
}
}
}

View file

@ -0,0 +1,127 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import java.awt.image.BufferedImage;
import java.io.IOException;
public class Image {
private final BufferedImage image;
private float width;
private float height;
private PDImageXObject imageXObject = null;
private final float[] dpi = {72, 72};
/**
* <p>
* Constructor for default images
* </p>
*
* @param image {@link BufferedImage}
*/
public Image(final BufferedImage image) {
this.image = image;
this.width = image.getWidth();
this.height = image.getHeight();
}
public Image(final BufferedImage image, float dpi) {
this(image, dpi, dpi);
}
public Image(final BufferedImage image, float dpiX, float dpiY) {
this.image = image;
this.width = image.getWidth();
this.height = image.getHeight();
this.dpi[0] = dpiX;
this.dpi[1] = dpiY;
scaleImageFromPixelToPoints();
}
/**
* <p>
* Drawing simple {@link Image} in {@link PDPageContentStream}.
* </p>
*
* @param doc {@link PDDocument} where drawing will be applied
* @param stream {@link PDPageContentStream} where drawing will be applied
* @param x X coordinate for image drawing
* @param y Y coordinate for image drawing
* @throws IOException if loading image fails
*/
public void draw(final PDDocument doc, final PageContentStreamOptimized stream, float x, float y) throws IOException {
if (imageXObject == null) {
imageXObject = LosslessFactory.createFromImage(doc, image);
}
stream.drawImage(imageXObject, x, y - height, width, height);
}
/**
* <p>
* Method which scale {@link Image} with designated width
* </p>
*
* @param width Maximal width where {@link Image} needs to be scaled
* @return Scaled {@link Image}
*/
public Image scaleByWidth(float width) {
float factorWidth = width / this.width;
return scale(width, this.height * factorWidth);
}
private void scaleImageFromPixelToPoints() {
float dpiX = dpi[0];
float dpiY = dpi[1];
scale(getImageWidthInPoints(dpiX), getImageHeightInPoints(dpiY));
}
/**
* <p>
* Method which scale {@link Image} with designated height
*
* @param height Maximal height where {@link Image} needs to be scaled
* @return Scaled {@link Image}
*/
public Image scaleByHeight(float height) {
float factorHeight = height / this.height;
return scale(this.width * factorHeight, height);
}
public float getImageWidthInPoints(float dpiX) {
return this.width * 72f / dpiX;
}
public float getImageHeightInPoints(float dpiY) {
return this.height * 72f / dpiY;
}
/**
* <p>
* Method which scale {@link Image} with designated width und height
*
* @param boundWidth Maximal width where {@link Image} needs to be scaled
* @param boundHeight Maximal height where {@link Image} needs to be scaled
* @return scaled {@link Image}
*/
public Image scale(float boundWidth, float boundHeight) {
float[] imageDimension = ImageUtils.getScaledDimension(this.width, this.height, boundWidth, boundHeight);
this.width = imageDimension[0];
this.height = imageDimension[1];
return this;
}
public float getHeight() {
return height;
}
public float getWidth() {
return width;
}
}

View file

@ -0,0 +1,56 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDPage;
public class ImageCell<T extends PDPage> extends Cell<T> {
private Image img;
private final HorizontalAlignment align;
private final VerticalAlignment valign;
ImageCell(Row<T> row, float width, Image image, boolean isCalculated) {
super(row, width, null, isCalculated);
this.img = image;
if (image.getWidth() > getInnerWidth()) {
scaleToFit();
}
this.align = HorizontalAlignment.LEFT;
this.valign = VerticalAlignment.TOP;
}
public void scaleToFit() {
img = img.scaleByWidth(getInnerWidth());
}
ImageCell(Row<T> row, float width, Image image, boolean isCalculated, HorizontalAlignment align,
VerticalAlignment valign) {
super(row, width, null, isCalculated, align, valign);
this.img = image;
if (image.getWidth() > getInnerWidth()) {
scaleToFit();
}
this.align = align;
this.valign = valign;
}
@Override
public float getTextHeight() {
return img.getHeight();
}
@Override
public float getHorizontalFreeSpace() {
return getInnerWidth() - img.getWidth();
}
@Override
public float getVerticalFreeSpace() {
return getInnerHeight() - img.getHeight();
}
public Image getImage() {
return img;
}
}

View file

@ -0,0 +1,69 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import java.awt.Dimension;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
/**
* <p>
* Utility methods for images
* </p>
*
* @author mkuehne
* @author hstimac
*/
public class ImageUtils {
// utility class, no instance needed
private ImageUtils() {
}
/**
* <p>
* Simple reading image from file
* </p>
*
* @param imageFile {@link File} from which image will be loaded
* @return {@link Image}
* @throws IOException if loading image fails
*/
public static Image readImage(File imageFile) throws IOException {
final BufferedImage bufferedImage = ImageIO.read(imageFile);
return new Image(bufferedImage);
}
/**
* <p>
* Provide an ability to scale {@link Image} on desired {@link Dimension}
* </p>
*
* @param imageWidth Original image width
* @param imageHeight Original image height
* @param boundWidth Desired image width
* @param boundHeight Desired image height
* @return {@code Array} with image dimension. First value is width and second is height.
*/
public static float[] getScaledDimension(float imageWidth, float imageHeight, float boundWidth, float boundHeight) {
float newImageWidth = imageWidth;
float newImageHeight = imageHeight;
// first check if we need to scale width
if (imageWidth > boundWidth) {
newImageWidth = boundWidth;
// scale height to maintain aspect ratio
newImageHeight = (newImageWidth * imageHeight) / imageWidth;
}
// then check if the new height is also bigger than expected
if (newImageHeight > boundHeight) {
newImageHeight = boundHeight;
// scale width to maintain aspect ratio
newImageWidth = (newImageHeight * imageWidth) / imageHeight;
}
float[] imageDimension = {newImageWidth, newImageHeight};
return imageDimension;
}
}

View file

@ -0,0 +1,122 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import java.awt.BasicStroke;
import java.awt.Color;
import java.util.Objects;
/**
* <p>
* The <code>LineStyle</code> class defines a basic set of rendering attributes
* for lines.
* </p>
*/
public class LineStyle {
private final Color color;
private final float width;
private float[] dashArray;
private float dashPhase;
/**
* <p>
* Simple constructor for setting line {@link Color} and line width
* </p>
*
* @param color The line {@link Color}
* @param width The line width
*/
public LineStyle(final Color color, final float width) {
this.color = color;
this.width = width;
}
/**
* <p>
* Provides ability to produce dotted line.
* </p>
*
* @param color The {@link Color} of the line
* @param width The line width
* @return new styled line
*/
public static LineStyle produceDotted(final Color color, final int width) {
final LineStyle line = new LineStyle(color, width);
line.dashArray = new float[]{1.0f};
line.dashPhase = 0.0f;
return line;
}
/**
* <p>
* Provides ability to produce dashed line.
* </p>
*
* @param color The {@link Color} of the line
* @param width The line width
* @return new styled line
*/
public static LineStyle produceDashed(final Color color, final int width) {
return produceDashed(color, width, new float[]{5.0f}, 0.0f);
}
/**
* @param color The {@link Color} of the line
* @param width The line width
* @param dashArray Mimics the behavior of {@link BasicStroke#getDashArray()}
* @param dashPhase Mimics the behavior of {@link BasicStroke#getDashPhase()}
* @return new styled line
*/
public static LineStyle produceDashed(final Color color, final int width, final float[] dashArray,
final float dashPhase) {
final LineStyle line = new LineStyle(color, width);
line.dashArray = dashArray;
line.dashPhase = dashPhase;
return line;
}
public Color getColor() {
return color;
}
public float getWidth() {
return width;
}
public float[] getDashArray() {
return dashArray;
}
public float getDashPhase() {
return dashPhase;
}
@Override
public int hashCode() {
int hash = 7;
hash = 89 * hash + Objects.hashCode(this.color);
hash = 89 * hash + Float.floatToIntBits(this.width);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final LineStyle other = (LineStyle) obj;
if (!Objects.equals(this.color, other.color)) {
return false;
}
return Float.floatToIntBits(this.width) == Float.floatToIntBits(other.width);
}
}

View file

@ -0,0 +1,112 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import java.awt.Color;
import java.io.IOException;
/**
* <p>
* Utility methods for {@link PDPageContentStream}
* </p>
*/
public final class PDStreamUtils {
private PDStreamUtils() {
}
/**
* <p>
* Provides ability to write on a {@link PDPageContentStream}. The text will
* be written above Y coordinate.
* </p>
*
* @param stream The {@link PDPageContentStream} where writing will be applied.
* @param text The text which will be displayed.
* @param font The font of the text
* @param fontSize The font size of the text
* @param x Start X coordinate for text.
* @param y Start Y coordinate for text.
* @param color Color of the text
*/
public static void write(final PageContentStreamOptimized stream, final String text, final PDFont font,
final float fontSize, final float x, final float y, final Color color) {
try {
stream.setFont(font, fontSize);
// we want to position our text on his baseline
stream.newLineAt(x, y - FontUtils.getDescent(font, fontSize) - FontUtils.getHeight(font, fontSize));
stream.setNonStrokingColor(color);
stream.showText(text);
} catch (final IOException e) {
throw new IllegalStateException("Unable to write text", e);
}
}
/**
* <p>
* Provides ability to draw rectangle for debugging purposes.
* </p>
*
* @param stream The {@link PDPageContentStream} where drawing will be applied.
* @param x Start X coordinate for rectangle.
* @param y Start Y coordinate for rectangle.
* @param width Width of rectangle
* @param height Height of rectangle
* @param color Color of the text
*/
public static void rect(final PageContentStreamOptimized stream, final float x, final float y, final float width,
final float height, final Color color) {
try {
stream.setNonStrokingColor(color);
// negative height because we want to draw down (not up!)
stream.addRect(x, y, width, -height);
stream.fill();
} catch (final IOException e) {
throw new IllegalStateException("Unable to draw rectangle", e);
}
}
/**
* <p>
* Provides ability to draw font metrics (font height, font ascent, font
* descent).
* </p>
*
* @param stream The {@link PDPageContentStream} where drawing will be applied.
* @param x Start X coordinate for rectangle.
* @param y Start Y coordinate for rectangle.
* @param font {@link PDFont} from which will be obtained font metrics
* @param fontSize Font size
*/
public static void rectFontMetrics(final PageContentStreamOptimized stream, final float x, final float y,
final PDFont font, final float fontSize) {
// height
PDStreamUtils.rect(stream, x, y, 3, FontUtils.getHeight(font, fontSize), Color.BLUE);
// ascent
PDStreamUtils.rect(stream, x + 3, y, 3, FontUtils.getAscent(font, fontSize), Color.CYAN);
// descent
PDStreamUtils.rect(stream, x + 3, y - FontUtils.getHeight(font, fontSize), 3, FontUtils.getDescent(font, 14),
Color.GREEN);
}
/**
* <p>
* Provides ability to set different line styles (line width, dotted line,
* dashed line)
* </p>
*
* @param stream The {@link PDPageContentStream} where drawing will be applied.
* @param line The {@link LineStyle} that would be applied
* @throws IOException If the content stream could not be written or the line color cannot be retrieved.
*/
public static void setLineStyles(final PageContentStreamOptimized stream, final LineStyle line) throws IOException {
stream.setStrokingColor(line.getColor());
stream.setLineWidth(line.getWidth());
stream.setLineCapStyle(0);
if (line.getDashArray() != null) {
stream.setLineDashPattern(line.getDashArray(), line.getDashPhase());
} else {
stream.setLineDashPattern(new float[]{}, 0.0f);
}
}
}

View file

@ -0,0 +1,174 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.util.Matrix;
import java.awt.Color;
import java.io.IOException;
import java.util.Arrays;
public class PageContentStreamOptimized {
private static final Matrix ROTATION = Matrix.getRotateInstance(Math.PI * 0.5, 0, 0);
private final PDPageContentStream pageContentStream;
private boolean textMode;
private float textCursorAbsoluteX;
private float textCursorAbsoluteY;
private boolean rotated;
public PageContentStreamOptimized(PDPageContentStream pageContentStream) {
this.pageContentStream = pageContentStream;
}
public void setRotated(boolean rotated) throws IOException {
if (this.rotated == rotated) {
return;
}
if (rotated) {
if (textMode) {
pageContentStream.setTextMatrix(ROTATION);
textCursorAbsoluteX = 0;
textCursorAbsoluteY = 0;
}
} else {
endText();
}
this.rotated = rotated;
}
public void beginText() throws IOException {
if (!textMode) {
pageContentStream.beginText();
if (rotated) {
pageContentStream.setTextMatrix(ROTATION);
}
textMode = true;
textCursorAbsoluteX = 0;
textCursorAbsoluteY = 0;
}
}
public void endText() throws IOException {
if (textMode) {
pageContentStream.endText();
textMode = false;
}
}
private PDFont currentFont;
private float currentFontSize;
public void setFont(PDFont font, float fontSize) throws IOException {
if (font != currentFont || fontSize != currentFontSize) {
pageContentStream.setFont(font, fontSize);
currentFont = font;
currentFontSize = fontSize;
}
}
public void showText(String text) throws IOException {
beginText();
pageContentStream.showText(text);
}
public void newLineAt(float tx, float ty) throws IOException {
beginText();
float dx = tx - textCursorAbsoluteX;
float dy = ty - textCursorAbsoluteY;
if (rotated) {
pageContentStream.newLineAtOffset(dy, -dx);
} else {
pageContentStream.newLineAtOffset(dx, dy);
}
textCursorAbsoluteX = tx;
textCursorAbsoluteY = ty;
}
public void drawImage(PDImageXObject image, float x, float y, float width, float height) throws IOException {
endText();
pageContentStream.drawImage(image, x, y, width, height);
}
private Color currentStrokingColor;
public void setStrokingColor(Color color) throws IOException {
if (color != currentStrokingColor) {
pageContentStream.setStrokingColor(color);
currentStrokingColor = color;
}
}
private Color currentNonStrokingColor;
public void setNonStrokingColor(Color color) throws IOException {
if (color != currentNonStrokingColor) {
pageContentStream.setNonStrokingColor(color);
currentNonStrokingColor = color;
}
}
public void addRect(float x, float y, float width, float height) throws IOException {
endText();
pageContentStream.addRect(x, y, width, height);
}
public void moveTo(float x, float y) throws IOException {
endText();
pageContentStream.moveTo(x, y);
}
public void lineTo(float x, float y) throws IOException {
endText();
pageContentStream.lineTo(x, y);
}
public void stroke() throws IOException {
endText();
pageContentStream.stroke();
}
public void fill() throws IOException {
endText();
pageContentStream.fill();
}
private float currentLineWidth = -1;
public void setLineWidth(float lineWidth) throws IOException {
if (lineWidth != currentLineWidth) {
endText();
pageContentStream.setLineWidth(lineWidth);
currentLineWidth = lineWidth;
}
}
private int currentLineCapStyle = -1;
public void setLineCapStyle(int lineCapStyle) throws IOException {
if (lineCapStyle != currentLineCapStyle) {
endText();
pageContentStream.setLineCapStyle(lineCapStyle);
currentLineCapStyle = lineCapStyle;
}
}
private float[] currentLineDashPattern;
private float currentLineDashPhase;
public void setLineDashPattern(float[] pattern, float phase) throws IOException {
if ((pattern != currentLineDashPattern &&
!Arrays.equals(pattern, currentLineDashPattern)) || phase != currentLineDashPhase) {
endText();
pageContentStream.setLineDashPattern(pattern, phase);
currentLineDashPattern = pattern;
currentLineDashPhase = phase;
}
}
public void close() throws IOException {
endText();
pageContentStream.close();
}
}

View file

@ -0,0 +1,15 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
public interface PageProvider<T extends PDPage> {
T createPage();
T nextPage();
T previousPage();
PDDocument getDocument();
}

View file

@ -0,0 +1,740 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Stack;
public class Paragraph {
private float width;
private final String text;
private float fontSize;
private PDFont font;
private final PDFont fontBold;
private final PDFont fontItalic;
private final PDFont fontBoldItalic;
private final WrappingFunction wrappingFunction;
private HorizontalAlignment align;
private final TextType textType;
private Color color;
private float lineSpacing;
private final static int DEFAULT_TAB = 4;
private final static int DEFAULT_TAB_AND_BULLET = 6;
private final static int BULLET_SPACE = 2;
private boolean drawDebug;
private final Map<Integer, Float> lineWidths = new HashMap<>();
private final Map<Integer, List<Token>> mapLineTokens = new LinkedHashMap<>();
private float maxLineWidth = Integer.MIN_VALUE;
private List<Token> tokens;
private List<String> lines;
private Float spaceWidth;
public Paragraph(String text, PDFont font, float fontSize, float width, final HorizontalAlignment align) {
this(text, font, fontSize, width, align, null);
}
// This function exists only to preserve backwards compatibility for
// the getWrappingFunction() method; it has been replaced with a faster implementation in the Tokenizer
private static final WrappingFunction DEFAULT_WRAP_FUNC = new WrappingFunction() {
@Override
public String[] getLines(String t) {
return t.split("(?<=\\s|-|@|,|\\.|:|;)");
}
};
public Paragraph(String text, PDFont font, int fontSize, int width) {
this(text, font, fontSize, width, HorizontalAlignment.LEFT, null);
}
public Paragraph(String text, PDFont font, float fontSize, float width, final HorizontalAlignment align,
WrappingFunction wrappingFunction) {
this(text, font, fontSize, width, align, Color.BLACK, null, wrappingFunction);
}
public Paragraph(String text, PDFont font, float fontSize, float width, final HorizontalAlignment align,
final Color color, final TextType textType, WrappingFunction wrappingFunction) {
this(text, font, fontSize, width, align, color, textType, wrappingFunction, 1);
}
public Paragraph(String text, PDFont font, float fontSize, float width, final HorizontalAlignment align,
final Color color, final TextType textType, WrappingFunction wrappingFunction, float lineSpacing) {
this.color = color;
this.text = text;
this.font = font;
// check if we have different default font for italic and bold text
if (FontUtils.getDefaultfonts().isEmpty()) {
fontBold = PDType1Font.HELVETICA_BOLD;
fontItalic = PDType1Font.HELVETICA_OBLIQUE;
fontBoldItalic = PDType1Font.HELVETICA_BOLD_OBLIQUE;
} else {
fontBold = FontUtils.getDefaultfonts().get("fontBold");
fontBoldItalic = FontUtils.getDefaultfonts().get("fontBoldItalic");
fontItalic = FontUtils.getDefaultfonts().get("fontItalic");
}
this.fontSize = fontSize;
this.width = width;
this.textType = textType;
this.setAlign(align);
this.wrappingFunction = wrappingFunction;
this.lineSpacing = lineSpacing;
}
public List<String> getLines() {
// memoize this function because it is very expensive
if (lines != null) {
return lines;
}
final List<String> result = new ArrayList<>();
// text and wrappingFunction are immutable, so we only ever need to compute tokens once
if (tokens == null) {
tokens = Tokenizer.tokenize(text, wrappingFunction);
}
int lineCounter = 0;
boolean italic = false;
boolean bold = false;
boolean listElement = false;
PDFont currentFont = font;
int orderListElement = 1;
int numberOfOrderedLists = 0;
int listLevel = 0;
Stack<HTMLListNode> stack = new Stack<>();
final PipelineLayer textInLine = new PipelineLayer();
final PipelineLayer sinceLastWrapPoint = new PipelineLayer();
for (final Token token : tokens) {
switch (token.getType()) {
case OPEN_TAG:
if (isBold(token)) {
bold = true;
currentFont = getFont(bold, italic);
} else if (isItalic(token)) {
italic = true;
currentFont = getFont(bold, italic);
} else if (isList(token)) {
listLevel++;
if (token.getData().equals("ol")) {
numberOfOrderedLists++;
if (listLevel > 1) {
stack.add(new HTMLListNode(orderListElement - 1, stack.isEmpty() ? (orderListElement - 1) + "." : stack.peek().getValue() + (orderListElement - 1) + "."));
}
orderListElement = 1;
textInLine.push(sinceLastWrapPoint);
// check if you have some text before this list, if you don't then you really don't need extra line break for that
if (textInLine.trimmedWidth() > 0) {
// this is our line
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
textInLine.reset();
lineCounter++;
}
} else if (token.getData().equals("ul")) {
textInLine.push(sinceLastWrapPoint);
// check if you have some text before this list, if you don't then you really don't need extra line break for that
if (textInLine.trimmedWidth() > 0) {
// this is our line
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
textInLine.reset();
lineCounter++;
}
}
}
sinceLastWrapPoint.push(token);
break;
case CLOSE_TAG:
if (isBold(token)) {
bold = false;
currentFont = getFont(bold, italic);
sinceLastWrapPoint.push(token);
} else if (isItalic(token)) {
italic = false;
currentFont = getFont(bold, italic);
sinceLastWrapPoint.push(token);
} else if (isList(token)) {
listLevel--;
if (token.getData().equals("ol")) {
numberOfOrderedLists--;
// reset elements
if (numberOfOrderedLists > 0) {
orderListElement = stack.peek().getOrderingNumber() + 1;
stack.pop();
}
}
// ensure extra space after each lists
// no need to worry about current line text because last closing <li> tag already done that
if (listLevel == 0) {
result.add(" ");
lineWidths.put(lineCounter, 0.0f);
mapLineTokens.put(lineCounter, new ArrayList<Token>());
lineCounter++;
}
} else if (isListElement(token)) {
// wrap at last wrap point?
if (textInLine.width() + sinceLastWrapPoint.trimmedWidth() > width) {
// this is our line
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
textInLine.reset();
lineCounter++;
// wrapping at last wrap point
if (numberOfOrderedLists > 0) {
String orderingNumber = stack.isEmpty() ? orderListElement + "." : stack.pop().getValue() + ".";
stack.add(new HTMLListNode(orderListElement, orderingNumber));
try {
float tab = indentLevel(DEFAULT_TAB);
float orderingNumberAndTab = font.getStringWidth(orderingNumber) + tab;
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String
.valueOf(orderingNumberAndTab / 1000 * getFontSize())));
} catch (IOException e) {
e.printStackTrace();
}
orderListElement++;
} else {
try {
// if it's not left aligned then ignore list and list element and deal with it as normal text where <li> mimic <br> behaviour
float tabBullet = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB * Math.max(listLevel - 1, 0) + DEFAULT_TAB_AND_BULLET) : indentLevel(DEFAULT_TAB);
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING,
String.valueOf(tabBullet / 1000 * getFontSize())));
} catch (IOException e) {
e.printStackTrace();
}
}
textInLine.push(sinceLastWrapPoint);
}
// wrapping at this must-have wrap point
textInLine.push(sinceLastWrapPoint);
// this is our line
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
textInLine.reset();
lineCounter++;
listElement = false;
}
if (isParagraph(token)) {
if (textInLine.width() + sinceLastWrapPoint.trimmedWidth() > width) {
// this is our line
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
lineCounter++;
textInLine.reset();
}
// wrapping at this must-have wrap point
textInLine.push(sinceLastWrapPoint);
// this is our line
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
textInLine.reset();
lineCounter++;
// extra spacing because it's a paragraph
result.add(" ");
lineWidths.put(lineCounter, 0.0f);
mapLineTokens.put(lineCounter, new ArrayList<Token>());
lineCounter++;
}
break;
case POSSIBLE_WRAP_POINT:
if (textInLine.width() + sinceLastWrapPoint.trimmedWidth() > width) {
// this is our line
if (!textInLine.isEmpty()) {
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
lineCounter++;
textInLine.reset();
}
// wrapping at last wrap point
if (listElement) {
if (numberOfOrderedLists > 0) {
try {
float tab = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB * Math.max(listLevel - 1, 0) + DEFAULT_TAB) : indentLevel(DEFAULT_TAB);
String orderingNumber = stack.isEmpty() ? orderListElement + "." : stack.peek().getValue() + "." + (orderListElement - 1) + ".";
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING,
String.valueOf((tab + font.getStringWidth(orderingNumber)) / 1000 * getFontSize())));
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
// if it's not left aligned then ignore list and list element and deal with it as normal text where <li> mimic <br> behavior
float tabBullet = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB * Math.max(listLevel - 1, 0) + DEFAULT_TAB_AND_BULLET) : indentLevel(DEFAULT_TAB);
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING,
String.valueOf(tabBullet / 1000 * getFontSize())));
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
textInLine.push(sinceLastWrapPoint);
} else {
textInLine.push(sinceLastWrapPoint);
}
break;
case WRAP_POINT:
// wrap at last wrap point?
if (textInLine.width() + sinceLastWrapPoint.trimmedWidth() > width) {
// this is our line
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
textInLine.reset();
lineCounter++;
// wrapping at last wrap point
if (listElement) {
if (!getAlign().equals(HorizontalAlignment.LEFT)) {
listLevel = 0;
}
if (numberOfOrderedLists > 0) {
// String orderingNumber = String.valueOf(orderListElement) + ". ";
String orderingNumber = stack.isEmpty() ? "1" + "." : stack.pop().getValue() + ". ";
try {
float tab = indentLevel(DEFAULT_TAB);
float orderingNumberAndTab = font.getStringWidth(orderingNumber) + tab;
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String
.valueOf(orderingNumberAndTab / 1000 * getFontSize())));
} catch (IOException e) {
e.printStackTrace();
}
} else {
try {
// if it's not left aligned then ignore list and list element and deal with it as normal text where <li> mimic <br> behaviour
float tabBullet = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB * Math.max(listLevel - 1, 0) + DEFAULT_TAB_AND_BULLET) : indentLevel(DEFAULT_TAB);
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING,
String.valueOf(tabBullet / 1000 * getFontSize())));
} catch (IOException e) {
e.printStackTrace();
}
}
}
textInLine.push(sinceLastWrapPoint);
}
if (isParagraph(token)) {
// check if you have some text before this paragraph, if you don't then you really don't need extra line break for that
if (textInLine.trimmedWidth() > 0) {
// extra spacing because it's a paragraph
result.add(" ");
lineWidths.put(lineCounter, 0.0f);
mapLineTokens.put(lineCounter, new ArrayList<Token>());
lineCounter++;
}
} else if (isListElement(token)) {
listElement = true;
// token padding, token bullet
try {
// if it's not left aligned then ignore list and list element and deal with it as normal text where <li> mimic <br> behaviour
float tab = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB * Math.max(listLevel - 1, 0) + DEFAULT_TAB) : indentLevel(DEFAULT_TAB);
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING,
String.valueOf(tab / 1000 * getFontSize())));
if (numberOfOrderedLists > 0) {
// if it's ordering list then move depending on your: ordering number + ". "
String orderingNumber;
if (listLevel > 1) {
orderingNumber = stack.peek().getValue() + orderListElement + ". ";
} else {
orderingNumber = orderListElement + ". ";
}
textInLine.push(currentFont, fontSize, Token.text(TokenType.ORDERING, orderingNumber));
orderListElement++;
} else {
// if it's unordered list then just move by bullet character (take care of alignment!)
textInLine.push(currentFont, fontSize, Token.text(TokenType.BULLET, " "));
}
} catch (IOException e) {
e.printStackTrace();
}
} else {
// wrapping at this must-have wrap point
textInLine.push(sinceLastWrapPoint);
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
textInLine.reset();
lineCounter++;
if (listLevel > 0) {
// preserve current indent
try {
if (numberOfOrderedLists > 0) {
float tab = getAlign().equals(HorizontalAlignment.LEFT) ? indentLevel(DEFAULT_TAB * Math.max(listLevel - 1, 0)) : indentLevel(DEFAULT_TAB);
// if it's ordering list then move depending on your: ordering number + ". "
String orderingNumber;
if (listLevel > 1) {
orderingNumber = stack.peek().getValue() + orderListElement + ". ";
} else {
orderingNumber = orderListElement + ". ";
}
float tabAndOrderingNumber = tab + font.getStringWidth(orderingNumber);
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING, String.valueOf(tabAndOrderingNumber / 1000 * getFontSize())));
orderListElement++;
} else {
if (getAlign().equals(HorizontalAlignment.LEFT)) {
float tab = indentLevel(DEFAULT_TAB * Math.max(listLevel - 1, 0) + DEFAULT_TAB + BULLET_SPACE);
textInLine.push(currentFont, fontSize, new Token(TokenType.PADDING,
String.valueOf(tab / 1000 * getFontSize())));
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
break;
case TEXT:
try {
String word = token.getData();
float wordWidth = token.getWidth(currentFont);
if (wordWidth / 1000f * fontSize > width && width > font.getAverageFontWidth() / 1000f * fontSize) {
// you need to check if you have already something in your line
boolean alreadyTextInLine = false;
if (textInLine.trimmedWidth() > 0) {
alreadyTextInLine = true;
}
while (wordWidth / 1000f * fontSize > width) {
float width = 0;
float firstPartWordWidth = 0;
float restOfTheWordWidth = 0;
String lastTextToken = word;
StringBuilder firstPartOfWord = new StringBuilder();
StringBuilder restOfTheWord = new StringBuilder();
for (int i = 0; i < lastTextToken.length(); i++) {
char c = lastTextToken.charAt(i);
try {
width += (currentFont.getStringWidth(String.valueOf(c)) / 1000f * fontSize);
} catch (IOException e) {
e.printStackTrace();
}
if (alreadyTextInLine) {
if (width < this.width - textInLine.trimmedWidth()) {
firstPartOfWord.append(c);
firstPartWordWidth = Math.max(width, firstPartWordWidth);
} else {
restOfTheWord.append(c);
restOfTheWordWidth = Math.max(width, restOfTheWordWidth);
}
} else {
if (width < this.width) {
firstPartOfWord.append(c);
firstPartWordWidth = Math.max(width, firstPartWordWidth);
} else {
if (i == 0) {
firstPartOfWord.append(c);
for (int j = 1; j < lastTextToken.length(); j++) {
restOfTheWord.append(lastTextToken.charAt(j));
}
break;
} else {
restOfTheWord.append(c);
restOfTheWordWidth = Math.max(width, restOfTheWordWidth);
}
}
}
}
// reset
alreadyTextInLine = false;
sinceLastWrapPoint.push(currentFont, fontSize,
Token.text(TokenType.TEXT, firstPartOfWord.toString()));
textInLine.push(sinceLastWrapPoint);
// this is our line
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
textInLine.reset();
lineCounter++;
word = restOfTheWord.toString();
wordWidth = currentFont.getStringWidth(word);
}
sinceLastWrapPoint.push(currentFont, fontSize, Token.text(TokenType.TEXT, word));
} else {
sinceLastWrapPoint.push(currentFont, fontSize, token);
}
} catch (IOException e) {
e.printStackTrace();
}
break;
}
}
if (sinceLastWrapPoint.trimmedWidth() + textInLine.trimmedWidth() > 0) {
textInLine.push(sinceLastWrapPoint);
result.add(textInLine.trimmedText());
lineWidths.put(lineCounter, textInLine.trimmedWidth());
mapLineTokens.put(lineCounter, textInLine.tokens());
maxLineWidth = Math.max(maxLineWidth, textInLine.trimmedWidth());
}
lines = result;
return result;
}
private static boolean isItalic(final Token token) {
return "i".equals(token.getData());
}
private static boolean isBold(final Token token) {
return "b".equals(token.getData());
}
private static boolean isParagraph(final Token token) {
return "p".equals(token.getData());
}
private static boolean isListElement(final Token token) {
return "li".equals(token.getData());
}
private static boolean isList(final Token token) {
return "ul".equals(token.getData()) || "ol".equals(token.getData());
}
private float indentLevel(int numberOfSpaces) throws IOException {
if (spaceWidth == null) {
spaceWidth = font.getSpaceWidth();
}
return numberOfSpaces * spaceWidth;
}
public PDFont getFont(boolean isBold, boolean isItalic) {
if (isBold) {
if (isItalic) {
return fontBoldItalic;
} else {
return fontBold;
}
} else if (isItalic) {
return fontItalic;
} else {
return font;
}
}
public float write(final PageContentStreamOptimized stream, float cursorX, float cursorY) {
if (drawDebug) {
PDStreamUtils.rectFontMetrics(stream, cursorX, cursorY, font, fontSize);
// width
PDStreamUtils.rect(stream, cursorX, cursorY, width, 1, Color.RED);
}
for (String line : getLines()) {
line = line.trim();
float textX = cursorX;
switch (align) {
case CENTER:
textX += getHorizontalFreeSpace(line) / 2;
break;
case LEFT:
break;
case RIGHT:
textX += getHorizontalFreeSpace(line);
break;
}
PDStreamUtils.write(stream, line, font, fontSize, textX, cursorY, color);
if (textType != null) {
switch (textType) {
case HIGHLIGHT:
case SQUIGGLY:
case STRIKEOUT:
throw new UnsupportedOperationException("Not implemented.");
case UNDERLINE:
float y = (float) (cursorY - FontUtils.getHeight(font, fontSize)
- FontUtils.getDescent(font, fontSize) - 1.5);
try {
float titleWidth = font.getStringWidth(line) / 1000 * fontSize;
stream.moveTo(textX, y);
stream.lineTo(textX + titleWidth, y);
stream.stroke();
} catch (final IOException e) {
throw new IllegalStateException("Unable to underline text", e);
}
break;
default:
break;
}
}
// move one "line" down
cursorY -= getFontHeight();
}
return cursorY;
}
public float getHeight() {
if (getLines().size() == 0) {
return 0;
} else {
return (getLines().size() - 1) * getLineSpacing() * getFontHeight() + getFontHeight();
}
}
public float getFontHeight() {
return FontUtils.getHeight(font, fontSize);
}
/**
* @return current font width
* @deprecated This method will be removed in a future release
*/
@Deprecated
public float getFontWidth() {
return font.getFontDescriptor().getFontBoundingBox().getWidth() / 1000 * fontSize;
}
/**
* @param width Paragraph's width
* @return {@link Paragraph} with designated width
* @deprecated This method will be removed in a future release
*/
@Deprecated
public Paragraph withWidth(int width) {
invalidateLineWrapping();
this.width = width;
return this;
}
/**
* @param font {@link PDFont} for {@link Paragraph}
* @param fontSize font size for {@link Paragraph}
* @return {@link Paragraph} with designated font and font size
* @deprecated This method will be removed in a future release
*/
@Deprecated
public Paragraph withFont(PDFont font, int fontSize) {
invalidateLineWrapping();
this.spaceWidth = null;
this.font = font;
this.fontSize = fontSize;
return this;
}
// font, fontSize, width, and align are non-final and used in getLines(),
// so if they are mutated, getLines() needs to be recomputed
private void invalidateLineWrapping() {
lines = null;
}
/**
* /**
*
* @param color {@code int} rgb value for color
* @return Paragraph's {@link Color}
* @deprecated This method will be removed in a future release
*/
@Deprecated
public Paragraph withColor(int color) {
this.color = new Color(color);
return this;
}
/**
* @return Paragraph's {@link Color}
* @deprecated This method will be replaced by
* {@code public Color getColor()} in a future release
*/
@Deprecated
public int getColor() {
return color.getRGB();
}
private float getHorizontalFreeSpace(final String text) {
try {
final float tw = font.getStringWidth(text.trim()) / 1000 * fontSize;
return width - tw;
} catch (IOException e) {
throw new IllegalStateException("Unable to calculate text width", e);
}
}
public float getWidth() {
return width;
}
public String getText() {
return text;
}
public float getFontSize() {
return fontSize;
}
public PDFont getFont() {
return font;
}
public HorizontalAlignment getAlign() {
return align;
}
public void setAlign(HorizontalAlignment align) {
invalidateLineWrapping();
this.align = align;
}
public boolean isDrawDebug() {
return drawDebug;
}
public void setDrawDebug(boolean drawDebug) {
this.drawDebug = drawDebug;
}
public WrappingFunction getWrappingFunction() {
return wrappingFunction == null ? DEFAULT_WRAP_FUNC : wrappingFunction;
}
public float getMaxLineWidth() {
return maxLineWidth;
}
public float getLineWidth(int key) {
return lineWidths.get(key);
}
public Map<Integer, List<Token>> getMapLineTokens() {
return mapLineTokens;
}
public float getLineSpacing() {
return lineSpacing;
}
public void setLineSpacing(float lineSpacing) {
this.lineSpacing = lineSpacing;
}
}

View file

@ -0,0 +1,134 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.font.PDFont;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* @author Markus Kühne
*/
public class PipelineLayer {
private static String rtrim(String s) {
int len = s.length();
while ((len > 0) && (s.charAt(len - 1) <= ' ')) {
len--;
}
if (len == s.length()) {
return s;
}
if (len == 0) {
return "";
}
return s.substring(0, len);
}
private final StringBuilder text = new StringBuilder();
private String lastTextToken = "";
private final List<Token> tokens = new ArrayList<>();
private String trimmedLastTextToken = "";
private float width;
private float widthLastToken;
private float widthTrimmedLastToken;
private float widthCurrentText;
public boolean isEmpty() {
return tokens.isEmpty();
}
public void push(final Token token) {
tokens.add(token);
}
public void push(final PDFont font, final float fontSize, final Token token) throws IOException {
if (token.getType().equals(TokenType.PADDING)) {
width += Float.parseFloat(token.getData());
}
if (token.getType().equals(TokenType.BULLET)) {
// just appending one space because our bullet width will be wide as one character of current font
text.append(token.getData());
width += (token.getWidth(font) / 1000f * fontSize);
}
if (token.getType().equals(TokenType.ORDERING)) {
// just appending one space because our bullet width will be wide as one character of current font
text.append(token.getData());
width += (token.getWidth(font) / 1000f * fontSize);
}
if (token.getType().equals(TokenType.TEXT)) {
text.append(lastTextToken);
width += widthLastToken;
lastTextToken = token.getData();
trimmedLastTextToken = rtrim(lastTextToken);
widthLastToken = token.getWidth(font) / 1000f * fontSize;
if (trimmedLastTextToken.length() == lastTextToken.length()) {
widthTrimmedLastToken = widthLastToken;
} else {
widthTrimmedLastToken = (font.getStringWidth(trimmedLastTextToken) / 1000f * fontSize);
}
widthCurrentText = text.length() == 0 ? 0 :
(font.getStringWidth(text.toString()) / 1000f * fontSize);
}
push(token);
}
public void push(final PipelineLayer pipeline) {
text.append(lastTextToken);
width += widthLastToken;
text.append(pipeline.text);
if (pipeline.text.length() > 0) {
width += pipeline.widthCurrentText;
}
lastTextToken = pipeline.lastTextToken;
trimmedLastTextToken = pipeline.trimmedLastTextToken;
widthLastToken = pipeline.widthLastToken;
widthTrimmedLastToken = pipeline.widthTrimmedLastToken;
tokens.addAll(pipeline.tokens);
pipeline.reset();
}
public void reset() {
text.delete(0, text.length());
width = 0.0f;
lastTextToken = "";
trimmedLastTextToken = "";
widthLastToken = 0.0f;
widthTrimmedLastToken = 0.0f;
tokens.clear();
}
public String trimmedText() {
return text.toString() + trimmedLastTextToken;
}
public float width() {
return width + widthLastToken;
}
public float trimmedWidth() {
return width + widthTrimmedLastToken;
}
public List<Token> tokens() {
return new ArrayList<>(tokens);
}
@Override
public String toString() {
return text.toString() + "(" + lastTextToken + ") [width: " + width() + ", trimmed: " + trimmedWidth() + "]";
}
}

View file

@ -0,0 +1,262 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class Row<T extends PDPage> {
private final Table<T> table;
PDOutlineItem bookmark;
List<Cell<T>> cells;
private boolean headerRow = false;
float height;
private float lineSpacing = 1;
Row(Table<T> table, List<Cell<T>> cells, float height) {
this.table = table;
this.cells = cells;
this.height = height;
}
Row(Table<T> table, float height) {
this.table = table;
this.cells = new ArrayList<>();
this.height = height;
}
/**
* <p>
* Creates a cell with provided width, cell value and default left top
* alignment
* </p>
*
* @param width Absolute width in points or in % of table width
* @param value Cell's value (content)
* @return New {@link Cell}
*/
public Cell<T> createCell(float width, String value) {
Cell<T> cell = new Cell<T>(this, width, value, true);
if (headerRow) {
// set all cell as header cell
cell.setHeaderCell(true);
}
setBorders(cell, cells.isEmpty());
cell.setLineSpacing(lineSpacing);
cells.add(cell);
return cell;
}
/**
* <p>
* Creates an image cell with provided width and {@link Image}
* </p>
*
* @param width Cell's width
* @param img {@link Image} in the cell
* @return {@link ImageCell}
*/
public ImageCell<T> createImageCell(float width, Image img) {
ImageCell<T> cell = new ImageCell<>(this, width, img, true);
setBorders(cell, cells.isEmpty());
cells.add(cell);
return cell;
}
public Cell<T> createImageCell(float width, Image img, HorizontalAlignment align, VerticalAlignment valign) {
Cell<T> cell = new ImageCell<T>(this, width, img, true, align, valign);
setBorders(cell, cells.isEmpty());
cells.add(cell);
return cell;
}
/**
* <p>
* Creates a table cell with provided width and table data
* </p>
*
* @param width Table width
* @param tableData Table's data (HTML table tags)
* @param doc {@link PDDocument} where this table will be drawn
* @param page {@link PDPage} where this table cell will be drawn
* @param yStart Y position from which table will be drawn
* @param pageTopMargin {@link TableCell}'s top margin
* @param pageBottomMargin {@link TableCell}'s bottom margin
* @return {@link TableCell} with provided width and table data
*/
public TableCell<T> createTableCell(float width, String tableData, PDDocument doc, PDPage page, float yStart,
float pageTopMargin, float pageBottomMargin) throws IOException {
TableCell<T> cell = new TableCell<T>(this, width, tableData, true, doc, page, yStart, pageTopMargin,
pageBottomMargin);
setBorders(cell, cells.isEmpty());
cells.add(cell);
return cell;
}
/**
* <p>
* Creates a cell with provided width, cell value, horizontal and vertical
* alignment
* </p>
*
* @param width Absolute width in points or in % of table width
* @param value Cell's value (content)
* @param align Cell's {@link HorizontalAlignment}
* @param valign Cell's {@link VerticalAlignment}
* @return New {@link Cell}
*/
public Cell<T> createCell(float width, String value, HorizontalAlignment align, VerticalAlignment valign) {
Cell<T> cell = new Cell<T>(this, width, value, true, align, valign);
if (headerRow) {
// set all cell as header cell
cell.setHeaderCell(true);
}
setBorders(cell, cells.isEmpty());
cell.setLineSpacing(lineSpacing);
cells.add(cell);
return cell;
}
/**
* <p>
* Creates a cell with the same width as the corresponding header cell
* </p>
*
* @param value Cell's value (content)
* @return new {@link Cell}
*/
public Cell<T> createCell(String value) {
float headerCellWidth = table.getHeader().getCells().get(cells.size()).getWidth();
Cell<T> cell = new Cell<T>(this, headerCellWidth, value, false);
setBorders(cell, cells.isEmpty());
cells.add(cell);
return cell;
}
/**
* <p>
* Remove left border to avoid double borders from previous cell's right
* border. In most cases left border will be removed.
* </p>
*
* @param cell {@link Cell}
* @param leftBorder boolean for drawing cell's left border. If {@code true} then
* the left cell's border will be drawn.
*/
private void setBorders(final Cell<T> cell, final boolean leftBorder) {
if (!leftBorder) {
cell.setLeftBorderStyle(null);
}
}
/**
* <p>
* remove top borders of cells to avoid double borders from cells in
* previous row
* </p>
*/
void removeTopBorders() {
for (final Cell<T> cell : cells) {
cell.setTopBorderStyle(null);
}
}
/**
* <p>
* Remove all borders of cells.
* </p>
*/
void removeAllBorders() {
for (final Cell<T> cell : cells) {
cell.setBorderStyle(null);
}
}
/**
* <p>
* Gets maximal height of the cells in current row therefore row's height.
* </p>
*
* @return Row's height
*/
public float getHeight() {
float maxheight = 0.0f;
for (Cell<T> cell : this.cells) {
float cellHeight = cell.getCellHeight();
if (cellHeight > maxheight) {
maxheight = cellHeight;
}
}
if (maxheight > height) {
this.height = maxheight;
}
return height;
}
public float getLineHeight() throws IOException {
return height;
}
public void setHeight(float height) {
this.height = height;
}
public List<Cell<T>> getCells() {
return cells;
}
public int getColCount() {
return cells.size();
}
public void setCells(List<Cell<T>> cells) {
this.cells = cells;
}
public float getWidth() {
return table.getWidth();
}
public PDOutlineItem getBookmark() {
return bookmark;
}
public void setBookmark(PDOutlineItem bookmark) {
this.bookmark = bookmark;
}
protected float getLastCellExtraWidth() {
float cellWidth = 0;
for (Cell<T> cell : cells) {
cellWidth += cell.getWidth();
}
float lastCellExtraWidth = this.getWidth() - cellWidth;
return lastCellExtraWidth;
}
public float xEnd() {
return table.getMargin() + getWidth();
}
public boolean isHeaderRow() {
return headerRow;
}
public void setHeaderRow(boolean headerRow) {
this.headerRow = headerRow;
}
public float getLineSpacing() {
return lineSpacing;
}
public void setLineSpacing(float lineSpacing) {
this.lineSpacing = lineSpacing;
}
}

View file

@ -0,0 +1,917 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
public abstract class Table<T extends PDPage> {
public final PDDocument document;
private final float margin;
private T currentPage;
private PageContentStreamOptimized tableContentStream;
private List<PDOutlineItem> bookmarks;
private final List<Row<T>> header = new ArrayList<>();
private final List<Row<T>> rows = new ArrayList<>();
private final float yStartNewPage;
private float yStart;
private final float width;
private final boolean drawLines;
private final boolean drawContent;
private final float headerBottomMargin = 4f;
private float lineSpacing = 1f;
private boolean tableIsBroken = false;
private boolean tableStartedAtNewPage = false;
private boolean removeTopBorders = false;
private boolean removeAllBorders = false;
private final PageProvider<T> pageProvider;
// page margins
private final float pageTopMargin;
private final float pageBottomMargin;
private boolean drawDebug;
/**
* @param yStart Y position where {@link Table} will start
* @param yStartNewPage Y position where possible new page of {@link Table} will start
* @param pageBottomMargin bottom margin of {@link Table}
* @param width {@link Table} width
* @param margin {@link Table} margin
* @param document {@link PDDocument} where {@link Table} will be drawn
* @param currentPage current page where {@link Table} will be drawn (some tables
* are big and can be through multiple pages)
* @param drawLines draw {@link Table}'s borders
* @param drawContent draw {@link Table}'s content
* @throws IOException if fonts are not loaded correctly
* @deprecated Use one of the constructors that pass a {@link PageProvider}
*/
@Deprecated
public Table(float yStart, float yStartNewPage, float pageBottomMargin, float width, float margin,
PDDocument document, T currentPage, boolean drawLines, boolean drawContent) throws IOException {
this(yStart, yStartNewPage, 0, pageBottomMargin, width, margin, document, currentPage, drawLines, drawContent,
null);
}
/**
* @param yStartNewPage Y position where possible new page of {@link Table} will start
* @param pageBottomMargin bottom margin of {@link Table}
* @param width {@link Table} width
* @param margin {@link Table} margin
* @param document {@link PDDocument} where {@link Table} will be drawn
* @param drawLines draw {@link Table}'s borders
* @param drawContent draw {@link Table}'s content
* @throws IOException if fonts are not loaded correctly
* @deprecated Use one of the constructors that pass a {@link PageProvider}
*/
@Deprecated
public Table(float yStartNewPage, float pageBottomMargin, float width, float margin, PDDocument document,
boolean drawLines, boolean drawContent) throws IOException {
this(yStartNewPage, 0, pageBottomMargin, width, margin, document, drawLines, drawContent, null);
}
public Table(float yStart, float yStartNewPage, float pageTopMargin, float pageBottomMargin, float width,
float margin, PDDocument document, T currentPage, boolean drawLines, boolean drawContent,
PageProvider<T> pageProvider) throws IOException {
this.pageTopMargin = pageTopMargin;
this.document = document;
this.drawLines = drawLines;
this.drawContent = drawContent;
// Initialize table
this.yStartNewPage = yStartNewPage;
this.margin = margin;
this.width = width;
this.yStart = yStart;
this.pageBottomMargin = pageBottomMargin;
this.currentPage = currentPage;
this.pageProvider = pageProvider;
loadFonts();
}
public Table(float yStartNewPage, float pageTopMargin, float pageBottomMargin, float width, float margin,
PDDocument document, boolean drawLines, boolean drawContent, PageProvider<T> pageProvider)
throws IOException {
this.pageTopMargin = pageTopMargin;
this.document = document;
this.drawLines = drawLines;
this.drawContent = drawContent;
// Initialize table
this.yStartNewPage = yStartNewPage;
this.margin = margin;
this.width = width;
this.pageProvider = pageProvider;
this.pageBottomMargin = pageBottomMargin;
// Fonts needs to be loaded before page creation
loadFonts();
this.currentPage = pageProvider.nextPage();
}
protected abstract void loadFonts() throws IOException;
protected PDType0Font loadFont(String fontPath) throws IOException {
return FontUtils.loadFont(getDocument(), fontPath);
}
protected PDDocument getDocument() {
return document;
}
public void drawTitle(String title, PDFont font, int fontSize, float tableWidth, float height, String alignment,
float freeSpaceForPageBreak, boolean drawHeaderMargin) throws IOException {
drawTitle(title, font, fontSize, tableWidth, height, alignment, freeSpaceForPageBreak, null, drawHeaderMargin);
}
public void drawTitle(String title, PDFont font, int fontSize, float tableWidth, float height, String alignment,
float freeSpaceForPageBreak, WrappingFunction wrappingFunction, boolean drawHeaderMargin)
throws IOException {
ensureStreamIsOpen();
if (isEndOfPage(freeSpaceForPageBreak)) {
this.tableContentStream.close();
pageBreak();
tableStartedAtNewPage = true;
}
if (title == null) {
// if you don't have title just use the height of maxTextBox in your
// "row"
yStart -= height;
} else {
PageContentStreamOptimized articleTitle = createPdPageContentStream();
Paragraph paragraph = new Paragraph(title, font, fontSize, tableWidth, HorizontalAlignment.get(alignment),
wrappingFunction);
paragraph.setDrawDebug(drawDebug);
yStart = paragraph.write(articleTitle, margin, yStart);
if (paragraph.getHeight() < height) {
yStart -= (height - paragraph.getHeight());
}
articleTitle.close();
if (drawDebug) {
// margin
PDStreamUtils.rect(tableContentStream, margin, yStart, width, headerBottomMargin, Color.CYAN);
}
}
if (drawHeaderMargin) {
yStart -= headerBottomMargin;
}
}
public float getWidth() {
return width;
}
public Row<T> createRow(float height) {
Row<T> row = new Row<T>(this, height);
row.setLineSpacing(lineSpacing);
this.rows.add(row);
return row;
}
public Row<T> createRow(List<Cell<T>> cells, float height) {
Row<T> row = new Row<T>(this, cells, height);
row.setLineSpacing(lineSpacing);
this.rows.add(row);
return row;
}
/**
* <p>
* Draws table
* </p>
*
* @return Y position of the table
* @throws IOException if underlying stream has problem being written to.
*/
public float draw() throws IOException {
ensureStreamIsOpen();
for (Row<T> row : rows) {
if (header.contains(row)) {
// check if header row height and first data row height can fit
// the page
// if not draw them on another side
if (isEndOfPage(getMinimumHeight())) {
pageBreak();
tableStartedAtNewPage = true;
}
}
drawRow(row);
}
endTable();
return yStart;
}
private void drawRow(Row<T> row) throws IOException {
// row.getHeight is currently an extremely expensive function so get the value
// once during drawing and reuse it, since it will not change during drawing
float rowHeight = row.getHeight();
// if it is not header row or first row in the table then remove row's
// top border
if (row != header && row != rows.get(0)) {
if (!isEndOfPage(rowHeight)) {
row.removeTopBorders();
}
}
// draw the bookmark
if (row.getBookmark() != null) {
PDPageXYZDestination bookmarkDestination = new PDPageXYZDestination();
bookmarkDestination.setPage(currentPage);
bookmarkDestination.setTop((int) yStart);
row.getBookmark().setDestination(bookmarkDestination);
this.addBookmark(row.getBookmark());
}
// we want to remove the borders as often as possible
removeTopBorders = true;
// check also if we want all borders removed
if (allBordersRemoved()) {
row.removeAllBorders();
}
if (isEndOfPage(rowHeight) && !header.contains(row)) {
// Draw line at bottom of table
endTable();
// insert page break
pageBreak();
// redraw all headers on each currentPage
if (!header.isEmpty()) {
for (Row<T> headerRow : header) {
drawRow(headerRow);
}
// after you draw all header rows on next page please keep
// removing top borders to avoid double border drawing
removeTopBorders = true;
} else {
// after a page break, we have to ensure that top borders get
// drawn
removeTopBorders = false;
}
}
// if it is first row in the table, we have to draw the top border
if (row == rows.get(0)) {
removeTopBorders = false;
}
if (removeTopBorders) {
row.removeTopBorders();
}
// if it is header row or first row in the table, we have to draw the
// top border
if (row == rows.get(0)) {
removeTopBorders = false;
}
if (removeTopBorders) {
row.removeTopBorders();
}
if (drawLines) {
drawVerticalLines(row, rowHeight);
}
if (drawContent) {
drawCellContent(row, rowHeight);
}
}
/**
* <p>
* Method to switch between the {@link PageProvider} and the abstract method
* {@link Table#createPage()}, preferring the {@link PageProvider}.
* </p>
* <p>
* Will be removed once {@link #createPage()} is removed.
* </p>
*
* @return
*/
private T createNewPage() {
if (pageProvider != null) {
return pageProvider.nextPage();
}
return createPage();
}
/**
* @return new {@link PDPage}
* @deprecated Use a {@link PageProvider} instead
*/
@Deprecated
// remove also createNewPage()
protected T createPage() {
throw new IllegalStateException(
"You either have to provide a " + PageProvider.class.getCanonicalName() + " or override this method");
}
private PageContentStreamOptimized createPdPageContentStream() throws IOException {
return new PageContentStreamOptimized(
new PDPageContentStream(getDocument(), getCurrentPage(),
PDPageContentStream.AppendMode.APPEND, true));
}
private void drawCellContent(Row<T> row, float rowHeight) throws IOException {
// position into first cell (horizontal)
float cursorX = margin;
float cursorY;
for (Cell<T> cell : row.getCells()) {
// remember horizontal cursor position, so we can advance to the
// next cell easily later
float cellStartX = cursorX;
if (cell instanceof ImageCell) {
final ImageCell<T> imageCell = (ImageCell<T>) cell;
cursorY = yStart - cell.getTopPadding()
- (cell.getTopBorder() == null ? 0 : cell.getTopBorder().getWidth());
// image cell vertical alignment
switch (cell.getValign()) {
case TOP:
break;
case MIDDLE:
cursorY -= cell.getVerticalFreeSpace() / 2;
break;
case BOTTOM:
cursorY -= cell.getVerticalFreeSpace();
break;
}
cursorX += cell.getLeftPadding() + (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth());
// image cell horizontal alignment
switch (cell.getAlign()) {
case CENTER:
cursorX += cell.getHorizontalFreeSpace() / 2;
break;
case LEFT:
break;
case RIGHT:
cursorX += cell.getHorizontalFreeSpace();
break;
}
imageCell.getImage().draw(document, tableContentStream, cursorX, cursorY);
} else if (cell instanceof TableCell) {
final TableCell<T> tableCell = (TableCell<T>) cell;
cursorY = yStart - cell.getTopPadding()
- (cell.getTopBorder() != null ? cell.getTopBorder().getWidth() : 0);
// table cell vertical alignment
switch (cell.getValign()) {
case TOP:
break;
case MIDDLE:
cursorY -= cell.getVerticalFreeSpace() / 2;
break;
case BOTTOM:
cursorY -= cell.getVerticalFreeSpace();
break;
}
cursorX += cell.getLeftPadding() + (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth());
tableCell.setXPosition(cursorX);
tableCell.setYPosition(cursorY);
this.tableContentStream.endText();
tableCell.draw(currentPage);
} else {
// no text without font
if (cell.getFont() == null) {
throw new IllegalArgumentException("Font is null on Cell=" + cell.getText());
}
if (cell.isTextRotated()) {
// debugging mode - drawing (default!) padding of rotated
// cells
// left
// PDStreamUtils.rect(tableContentStream, cursorX, yStart,
// 5, cell.getHeight(), Color.GREEN);
// top
// PDStreamUtils.rect(tableContentStream, cursorX, yStart,
// cell.getWidth(), 5 , Color.GREEN);
// bottom
// PDStreamUtils.rect(tableContentStream, cursorX, yStart -
// cell.getHeight(), cell.getWidth(), -5 , Color.GREEN);
// right
// PDStreamUtils.rect(tableContentStream, cursorX +
// cell.getWidth() - 5, yStart, 5, cell.getHeight(),
// Color.GREEN);
cursorY = yStart - cell.getInnerHeight() - cell.getTopPadding()
- (cell.getTopBorder() != null ? cell.getTopBorder().getWidth() : 0);
switch (cell.getAlign()) {
case CENTER:
cursorY += cell.getVerticalFreeSpace() / 2;
break;
case LEFT:
break;
case RIGHT:
cursorY += cell.getVerticalFreeSpace();
break;
}
// respect left padding and descend by font height to get
// position of the base line
cursorX += cell.getLeftPadding()
+ (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth())
+ FontUtils.getHeight(cell.getFont(), cell.getFontSize())
+ FontUtils.getDescent(cell.getFont(), cell.getFontSize());
switch (cell.getValign()) {
case TOP:
break;
case MIDDLE:
cursorX += cell.getHorizontalFreeSpace() / 2;
break;
case BOTTOM:
cursorX += cell.getHorizontalFreeSpace();
break;
}
// make tokenize method just in case
cell.getParagraph().getLines();
} else {
// debugging mode - drawing (default!) padding of rotated
// cells
// left
// PDStreamUtils.rect(tableContentStream, cursorX, yStart,
// 5, cell.getHeight(), Color.RED);
// top
// PDStreamUtils.rect(tableContentStream, cursorX, yStart,
// cell.getWidth(), 5 , Color.RED);
// bottom
// PDStreamUtils.rect(tableContentStream, cursorX, yStart -
// cell.getHeight(), cell.getWidth(), -5 , Color.RED);
// right
// PDStreamUtils.rect(tableContentStream, cursorX +
// cell.getWidth() - 5, yStart, 5, cell.getHeight(),
// Color.RED);
// position at top of current cell descending by font height
// - font descent, because we are
// positioning the base line here
cursorY = yStart - cell.getTopPadding() - FontUtils.getHeight(cell.getFont(), cell.getFontSize())
- FontUtils.getDescent(cell.getFont(), cell.getFontSize())
- (cell.getTopBorder() == null ? 0 : cell.getTopBorder().getWidth());
if (drawDebug) {
// @formatter:off
// top padding
PDStreamUtils.rect(tableContentStream, cursorX + (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth()), yStart - (cell.getTopBorder() == null ? 0 : cell.getTopBorder().getWidth()), cell.getWidth() - (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth()) - (cell.getRightBorder() == null ? 0 : cell.getRightBorder().getWidth()), cell.getTopPadding(), Color.RED);
// bottom padding
PDStreamUtils.rect(tableContentStream, cursorX + (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth()), yStart - cell.getHeight() + (cell.getBottomBorder() == null ? 0 : cell.getBottomBorder().getWidth()) + cell.getBottomPadding(), cell.getWidth() - (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth()) - (cell.getRightBorder() == null ? 0 : cell.getRightBorder().getWidth()), cell.getBottomPadding(), Color.RED);
// left padding
PDStreamUtils.rect(tableContentStream, cursorX + (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth()), yStart - (cell.getTopBorder() == null ? 0 : cell.getTopBorder().getWidth()), cell.getLeftPadding(), cell.getHeight() - (cell.getTopBorder() == null ? 0 : cell.getTopBorder().getWidth()) - (cell.getBottomBorder() == null ? 0 : cell.getBottomBorder().getWidth()), Color.RED);
// right padding
PDStreamUtils.rect(tableContentStream, cursorX + cell.getWidth() - (cell.getRightBorder() == null ? 0 : cell.getRightBorder().getWidth()), yStart - (cell.getTopBorder() == null ? 0 : cell.getTopBorder().getWidth()), -cell.getRightPadding(), cell.getHeight() - (cell.getTopBorder() == null ? 0 : cell.getTopBorder().getWidth()) - (cell.getBottomBorder() == null ? 0 : cell.getBottomBorder().getWidth()), Color.RED);
// @formatter:on
}
// respect left padding
cursorX += cell.getLeftPadding()
+ (cell.getLeftBorder() == null ? 0 : cell.getLeftBorder().getWidth());
// the widest text does not fill the inner width of the
// cell? no
// problem, just add it ;)
switch (cell.getAlign()) {
case CENTER:
cursorX += cell.getHorizontalFreeSpace() / 2;
break;
case LEFT:
break;
case RIGHT:
cursorX += cell.getHorizontalFreeSpace();
break;
}
switch (cell.getValign()) {
case TOP:
break;
case MIDDLE:
cursorY -= cell.getVerticalFreeSpace() / 2;
break;
case BOTTOM:
cursorY -= cell.getVerticalFreeSpace();
break;
}
}
// remember this horizontal position, as it is the anchor for
// each
// new line
float lineStartX = cursorX;
float lineStartY = cursorY;
this.tableContentStream.setNonStrokingColor(cell.getTextColor());
int italicCounter = 0;
int boldCounter = 0;
this.tableContentStream.setRotated(cell.isTextRotated());
// print all lines of the cell
for (Map.Entry<Integer, List<Token>> entry : cell.getParagraph().getMapLineTokens().entrySet()) {
// calculate the width of this line
float freeSpaceWithinLine = cell.getParagraph().getMaxLineWidth()
- cell.getParagraph().getLineWidth(entry.getKey());
// TODO: need to implemented rotated text yo!
if (cell.isTextRotated()) {
cursorY = lineStartY;
switch (cell.getAlign()) {
case CENTER:
cursorY += freeSpaceWithinLine / 2;
break;
case LEFT:
break;
case RIGHT:
cursorY += freeSpaceWithinLine;
break;
}
} else {
cursorX = lineStartX;
switch (cell.getAlign()) {
case CENTER:
cursorX += freeSpaceWithinLine / 2;
break;
case LEFT:
// it doesn't matter because X position is always
// the same
// as row above
break;
case RIGHT:
cursorX += freeSpaceWithinLine;
break;
}
}
// iterate through tokens in current line
PDFont currentFont = cell.getParagraph().getFont(false, false);
for (Token token : entry.getValue()) {
switch (token.getType()) {
case OPEN_TAG:
if ("b".equals(token.getData())) {
boldCounter++;
} else if ("i".equals(token.getData())) {
italicCounter++;
}
break;
case CLOSE_TAG:
if ("b".equals(token.getData())) {
boldCounter = Math.max(boldCounter - 1, 0);
} else if ("i".equals(token.getData())) {
italicCounter = Math.max(italicCounter - 1, 0);
}
break;
case PADDING:
cursorX += Float.parseFloat(token.getData());
break;
case ORDERING:
currentFont = cell.getParagraph().getFont(boldCounter > 0, italicCounter > 0);
this.tableContentStream.setFont(currentFont, cell.getFontSize());
if (cell.isTextRotated()) {
tableContentStream.newLineAt(cursorX, cursorY);
this.tableContentStream.showText(token.getData());
cursorY += token.getWidth(currentFont) / 1000 * cell.getFontSize();
} else {
this.tableContentStream.newLineAt(cursorX, cursorY);
this.tableContentStream.showText(token.getData());
cursorX += token.getWidth(currentFont) / 1000 * cell.getFontSize();
}
break;
case BULLET:
float widthOfSpace = currentFont.getSpaceWidth();
float halfHeight = FontUtils.getHeight(currentFont, cell.getFontSize()) / 2;
if (cell.isTextRotated()) {
PDStreamUtils.rect(tableContentStream, cursorX + halfHeight, cursorY,
token.getWidth(currentFont) / 1000 * cell.getFontSize(),
widthOfSpace / 1000 * cell.getFontSize(),
cell.getTextColor());
// move cursorY for two characters (one for
// bullet, one for space after bullet)
cursorY += 2 * widthOfSpace / 1000 * cell.getFontSize();
} else {
PDStreamUtils.rect(tableContentStream, cursorX, cursorY + halfHeight,
token.getWidth(currentFont) / 1000 * cell.getFontSize(),
widthOfSpace / 1000 * cell.getFontSize(),
cell.getTextColor());
// move cursorX for two characters (one for
// bullet, one for space after bullet)
cursorX += 2 * widthOfSpace / 1000 * cell.getFontSize();
}
break;
case TEXT:
currentFont = cell.getParagraph().getFont(boldCounter > 0, italicCounter > 0);
this.tableContentStream.setFont(currentFont, cell.getFontSize());
if (cell.isTextRotated()) {
tableContentStream.newLineAt(cursorX, cursorY);
this.tableContentStream.showText(token.getData());
cursorY += token.getWidth(currentFont) / 1000 * cell.getFontSize();
} else {
try {
this.tableContentStream.newLineAt(cursorX, cursorY);
this.tableContentStream.showText(token.getData());
cursorX += token.getWidth(currentFont) / 1000 * cell.getFontSize();
} catch (IOException e) {
e.printStackTrace();
}
}
break;
}
}
if (cell.isTextRotated()) {
cursorX = cursorX + cell.getParagraph().getFontHeight() * cell.getLineSpacing();
} else {
cursorY = cursorY - cell.getParagraph().getFontHeight() * cell.getLineSpacing();
}
}
}
// set cursor to the start of this cell plus its width to advance to
// the next cell
cursorX = cellStartX + cell.getWidth();
}
// Set Y position for next row
yStart = yStart - rowHeight;
}
private void drawVerticalLines(Row<T> row, float rowHeight) throws IOException {
float xStart = margin;
Iterator<Cell<T>> cellIterator = row.getCells().iterator();
while (cellIterator.hasNext()) {
Cell<T> cell = cellIterator.next();
float cellWidth = cellIterator.hasNext()
? cell.getWidth()
: this.width - (xStart - margin);
fillCellColor(cell, yStart, xStart, rowHeight, cellWidth);
drawCellBorders(rowHeight, cell, xStart);
xStart += cellWidth;
}
}
private void drawCellBorders(float rowHeight, Cell<T> cell, float xStart) throws IOException {
float yEnd = yStart - rowHeight;
// top
LineStyle topBorder = cell.getTopBorder();
if (topBorder != null) {
float y = yStart - topBorder.getWidth() / 2;
drawLine(xStart, y, xStart + cell.getWidth(), y, topBorder);
}
// right
LineStyle rightBorder = cell.getRightBorder();
if (rightBorder != null) {
float x = xStart + cell.getWidth() - rightBorder.getWidth() / 2;
drawLine(x, yStart - (topBorder == null ? 0 : topBorder.getWidth()), x, yEnd, rightBorder);
}
// bottom
LineStyle bottomBorder = cell.getBottomBorder();
if (bottomBorder != null) {
float y = yEnd + bottomBorder.getWidth() / 2;
drawLine(xStart, y, xStart + cell.getWidth() - (rightBorder == null ? 0 : rightBorder.getWidth()), y,
bottomBorder);
}
// left
LineStyle leftBorder = cell.getLeftBorder();
if (leftBorder != null) {
float x = xStart + leftBorder.getWidth() / 2;
drawLine(x, yStart, x, yEnd + (bottomBorder == null ? 0 : bottomBorder.getWidth()), leftBorder);
}
}
private void drawLine(float xStart, float yStart, float xEnd, float yEnd, LineStyle border) throws IOException {
PDStreamUtils.setLineStyles(tableContentStream, border);
tableContentStream.moveTo(xStart, yStart);
tableContentStream.lineTo(xEnd, yEnd);
tableContentStream.stroke();
}
private void fillCellColor(Cell<T> cell, float yStart, float xStart, float rowHeight, float cellWidth)
throws IOException {
if (cell.getFillColor() != null) {
this.tableContentStream.setNonStrokingColor(cell.getFillColor());
// y start is bottom pos
yStart = yStart - rowHeight;
float height = rowHeight - (cell.getTopBorder() == null ? 0 : cell.getTopBorder().getWidth());
this.tableContentStream.addRect(xStart, yStart, cellWidth, height);
this.tableContentStream.fill();
}
}
private void ensureStreamIsOpen() throws IOException {
if (tableContentStream == null) {
tableContentStream = createPdPageContentStream();
}
}
private void endTable() throws IOException {
this.tableContentStream.close();
}
public T getCurrentPage() {
return this.currentPage;
}
private boolean isEndOfPage(float freeSpaceForPageBreak) {
float currentY = yStart - freeSpaceForPageBreak;
boolean isEndOfPage = currentY <= pageBottomMargin;
if (isEndOfPage) {
setTableIsBroken(true);
}
return isEndOfPage;
}
private void pageBreak() throws IOException {
tableContentStream.close();
this.yStart = yStartNewPage - pageTopMargin;
this.currentPage = createNewPage();
this.tableContentStream = createPdPageContentStream();
}
private void addBookmark(PDOutlineItem bookmark) {
if (bookmarks == null) {
bookmarks = new ArrayList<>();
}
bookmarks.add(bookmark);
}
public List<PDOutlineItem> getBookmarks() {
return bookmarks;
}
/**
* /**
*
* @param header row that will be set as table's header row
* @deprecated Use {@link #addHeaderRow(Row)} instead, as it supports
* multiple header rows
*/
@Deprecated
public void setHeader(Row<T> header) {
this.header.clear();
addHeaderRow(header);
}
/**
* <p>
* Calculate height of all table cells (essentially, table height).
* </p>
* <p>
* IMPORTANT: Doesn't acknowledge possible page break. Use with caution.
* </p>
*
* @return {@link Table}'s height
*/
public float getHeaderAndDataHeight() {
float height = 0;
for (Row<T> row : rows) {
height += row.getHeight();
}
return height;
}
/**
* <p>
* Calculates minimum table height that needs to be drawn (all header rows +
* first data row heights).
* </p>
*
* @return height
*/
public float getMinimumHeight() {
float height = 0.0f;
int firstDataRowIndex = 0;
if (!header.isEmpty()) {
for (Row<T> headerRow : header) {
// count all header rows height
height += headerRow.getHeight();
firstDataRowIndex++;
}
}
if (rows.size() > firstDataRowIndex) {
height += rows.get(firstDataRowIndex).getHeight();
}
return height;
}
/**
* <p>
* Setting current row as table header row
* </p>
*
* @param row The row that would be added as table's header row
*/
public void addHeaderRow(Row<T> row) {
this.header.add(row);
row.setHeaderRow(true);
}
/**
* <p>
* Retrieves last table's header row
* </p>
*
* @return header row
*/
public Row<T> getHeader() {
if (header == null) {
throw new IllegalArgumentException("Header Row not set on table");
}
return header.get(header.size() - 1);
}
public float getMargin() {
return margin;
}
protected void setYStart(float yStart) {
this.yStart = yStart;
}
public boolean isDrawDebug() {
return drawDebug;
}
public void setDrawDebug(boolean drawDebug) {
this.drawDebug = drawDebug;
}
public boolean tableIsBroken() {
return tableIsBroken;
}
public void setTableIsBroken(boolean tableIsBroken) {
this.tableIsBroken = tableIsBroken;
}
public List<Row<T>> getRows() {
return rows;
}
public boolean tableStartedAtNewPage() {
return tableStartedAtNewPage;
}
public float getLineSpacing() {
return lineSpacing;
}
public void setLineSpacing(float lineSpacing) {
this.lineSpacing = lineSpacing;
}
public boolean allBordersRemoved() {
return removeAllBorders;
}
public void removeAllBorders(boolean removeAllBorders) {
this.removeAllBorders = removeAllBorders;
}
}

View file

@ -0,0 +1,339 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.font.PDFont;
import java.io.IOException;
import java.util.List;
import java.util.Map;
public class TableCell<T extends PDPage> extends Cell<T> {
private final String tableData;
private final float width;
private float yStart;
private float xStart;
private float height = 0;
private final PDDocument doc;
private final PDPage page;
private final float marginBetweenElementsY = FontUtils.getHeight(getFont(), getFontSize());
private final HorizontalAlignment align;
private final VerticalAlignment valign;
// default FreeSans font
// private PDFont font = FontUtils.getDefaultfonts().get("font");
// private PDFont fontBold = FontUtils.getDefaultfonts().get("fontBold");
private PageContentStreamOptimized tableCellContentStream;
// page margins
private final float pageTopMargin;
private final float pageBottomMargin;
// default title fonts
private final int tableTitleFontSize = 8;
TableCell(Row<T> row, float width, String tableData, boolean isCalculated, PDDocument document, PDPage page,
float yStart, float pageTopMargin, float pageBottomMargin) throws IOException {
this(row, width, tableData, isCalculated, document, page, yStart, pageTopMargin, pageBottomMargin,
HorizontalAlignment.LEFT, VerticalAlignment.TOP);
}
TableCell(Row<T> row, float width, String tableData, boolean isCalculated, PDDocument document, PDPage page,
float yStart, float pageTopMargin, float pageBottomMargin, final HorizontalAlignment align,
final VerticalAlignment valign) throws IOException {
super(row, width, tableData, isCalculated);
this.tableData = tableData;
this.width = width * row.getWidth() / 100;
this.doc = document;
this.page = page;
this.yStart = yStart;
this.pageTopMargin = pageTopMargin;
this.pageBottomMargin = pageBottomMargin;
this.align = align;
this.valign = valign;
fillTable();
}
/**
* <p>
* This method just fills up the table's with her content for proper table
* cell height calculation. Position of the table (x,y) is not relevant
* here.
* </p>
* <p>
* NOTE: if entire row is not header row then use bold instead header cell (
* {@code
*
* <th>})
* </p>
*/
public void fillTable() throws IOException {
// please consider the cell's paddings
float tableWidth = this.width - getLeftPadding() - getRightPadding();
tableCellContentStream = new PageContentStreamOptimized(new PDPageContentStream(doc, page,
PDPageContentStream.AppendMode.APPEND, true));
// check if there is some additional text outside inner table
String[] outerTableText = tableData.split("<table");
// don't forget to attach splited tag
for (int i = 1; i < outerTableText.length; i++) {
outerTableText[i] = "<table " + outerTableText[i];
}
Paragraph outerTextParagraph = null;
String caption = "";
height = 0;
height = (getTopBorder() == null ? 0 : getTopBorder().getWidth()) + getTopPadding();
for (String element : outerTableText) {
if (element.contains("</table")) {
String[] chunks = element.split("</table>");
for (String chunkie : chunks) {
// make paragraph and get tokens
outerTextParagraph = new Paragraph(chunkie, getFont(), 8, (int) tableWidth);
outerTextParagraph.getLines();
height += (outerTextParagraph != null
? outerTextParagraph.getHeight() + marginBetweenElementsY : 0);
yStart = writeOrCalculateParagraph(outerTextParagraph, true);
}
} else {
// make paragraph and get tokens
outerTextParagraph = new Paragraph(element, getFont(), 8, (int) tableWidth);
outerTextParagraph.getLines();
height += (outerTextParagraph != null ? outerTextParagraph.getHeight() + marginBetweenElementsY
: 0);
yStart = writeOrCalculateParagraph(outerTextParagraph, true);
}
}
tableCellContentStream.close();
}
/**
* <p>
* Method provides writing or height calculation of possible outer text
* </p>
*
* @param paragraph Paragraph that needs to be written or whose height needs to be
* calculated
* @param onlyCalculateHeight if <code>true</code> the given paragraph will not be drawn
* just his height will be calculated.
* @return Y position after calculating/writing given paragraph
*/
private float writeOrCalculateParagraph(Paragraph paragraph, boolean onlyCalculateHeight) throws IOException {
int boldCounter = 0;
int italicCounter = 0;
if (!onlyCalculateHeight) {
tableCellContentStream.setRotated(isTextRotated());
}
// position at top of current cell descending by font height - font
// descent, because we are positioning the base line here
float cursorY = yStart - getTopPadding() - FontUtils.getHeight(getFont(), getFontSize())
- FontUtils.getDescent(getFont(), getFontSize()) - (getTopBorder() == null ? 0 : getTopBorder().getWidth());
float cursorX = xStart;
// loop through tokens
for (Map.Entry<Integer, List<Token>> entry : paragraph.getMapLineTokens().entrySet()) {
// calculate the width of this line
float freeSpaceWithinLine = paragraph.getMaxLineWidth() - paragraph.getLineWidth(entry.getKey());
if (isTextRotated()) {
switch (align) {
case CENTER:
cursorY += freeSpaceWithinLine / 2;
break;
case LEFT:
break;
case RIGHT:
cursorY += freeSpaceWithinLine;
break;
}
} else {
switch (align) {
case CENTER:
cursorX += freeSpaceWithinLine / 2;
break;
case LEFT:
// it doesn't matter because X position is always the same
// as row above
break;
case RIGHT:
cursorX += freeSpaceWithinLine;
break;
}
}
// iterate through tokens in current line
PDFont currentFont = paragraph.getFont(false, false);
for (Token token : entry.getValue()) {
switch (token.getType()) {
case OPEN_TAG:
if ("b".equals(token.getData())) {
boldCounter++;
} else if ("i".equals(token.getData())) {
italicCounter++;
}
break;
case CLOSE_TAG:
if ("b".equals(token.getData())) {
boldCounter = Math.max(boldCounter - 1, 0);
} else if ("i".equals(token.getData())) {
italicCounter = Math.max(italicCounter - 1, 0);
}
break;
case PADDING:
cursorX += Float.parseFloat(token.getData());
break;
case ORDERING:
currentFont = paragraph.getFont(boldCounter > 0, italicCounter > 0);
tableCellContentStream.setFont(currentFont, getFontSize());
if (isTextRotated()) {
// if it is not calculation then draw it
if (!onlyCalculateHeight) {
tableCellContentStream.newLineAt(cursorX, cursorY);
tableCellContentStream.showText(token.getData());
}
cursorY += token.getWidth(currentFont) / 1000 * getFontSize();
} else {
// if it is not calculation then draw it
if (!onlyCalculateHeight) {
tableCellContentStream.newLineAt(cursorX, cursorY);
tableCellContentStream.showText(token.getData());
}
cursorX += token.getWidth(currentFont) / 1000 * getFontSize();
}
break;
case BULLET:
float widthOfSpace = currentFont.getSpaceWidth();
float halfHeight = FontUtils.getHeight(currentFont, getFontSize()) / 2;
if (isTextRotated()) {
if (!onlyCalculateHeight) {
PDStreamUtils.rect(tableCellContentStream, cursorX + halfHeight, cursorY,
token.getWidth(currentFont) / 1000 * getFontSize(),
widthOfSpace / 1000 * getFontSize(), getTextColor());
}
// move cursorY for two characters (one for bullet, one
// for space after bullet)
cursorY += 2 * widthOfSpace / 1000 * getFontSize();
} else {
if (!onlyCalculateHeight) {
PDStreamUtils.rect(tableCellContentStream, cursorX, cursorY + halfHeight,
token.getWidth(currentFont) / 1000 * getFontSize(),
widthOfSpace / 1000 * getFontSize(), getTextColor());
}
// move cursorX for two characters (one for bullet, one
// for space after bullet)
cursorX += 2 * widthOfSpace / 1000 * getFontSize();
}
break;
case TEXT:
currentFont = paragraph.getFont(boldCounter > 0, italicCounter > 0);
tableCellContentStream.setFont(currentFont, getFontSize());
if (isTextRotated()) {
if (!onlyCalculateHeight) {
tableCellContentStream.newLineAt(cursorX, cursorY);
tableCellContentStream.showText(token.getData());
}
cursorY += token.getWidth(currentFont) / 1000 * getFontSize();
} else {
if (!onlyCalculateHeight) {
tableCellContentStream.newLineAt(cursorX, cursorY);
tableCellContentStream.showText(token.getData());
}
cursorX += token.getWidth(currentFont) / 1000 * getFontSize();
}
break;
}
}
// reset
cursorX = xStart;
cursorY -= FontUtils.getHeight(getFont(), getFontSize());
}
return cursorY;
}
/**
* <p>
* This method draw table cell with proper X,Y position which are determined
* in {@link Table#draw()} method
* </p>
* <p>
* NOTE: if entire row is not header row then use bold instead header cell (
* {@code
*
* <th>})
* </p>
*
* @param page {@link PDPage} where table cell be written on
*/
public void draw(PDPage page) throws IOException {
// please consider the cell's paddings
float tableWidth = this.width - getLeftPadding() - getRightPadding();
tableCellContentStream = new PageContentStreamOptimized(new PDPageContentStream(doc, page,
PDPageContentStream.AppendMode.APPEND, true));
// check if there is some additional text outside inner table
String[] outerTableText = tableData.split("<table");
// don't forget to attach splited tag
for (int i = 1; i < outerTableText.length; i++) {
outerTableText[i] = "<table " + outerTableText[i];
}
Paragraph outerTextParagraph = null;
String caption = "";
height = 0;
height = (getTopBorder() == null ? 0 : getTopBorder().getWidth()) + getTopPadding();
for (String element : outerTableText) {
if (element.contains("</table")) {
String[] chunks = element.split("</table>");
for (String chunkie : chunks) {
// make paragraph and get tokens
outerTextParagraph = new Paragraph(chunkie, getFont(), 8, (int) tableWidth);
outerTextParagraph.getLines();
height += (outerTextParagraph != null
? outerTextParagraph.getHeight() + marginBetweenElementsY : 0);
yStart = writeOrCalculateParagraph(outerTextParagraph, false);
}
} else {
// make paragraph and get tokens
outerTextParagraph = new Paragraph(element, getFont(), 8, (int) tableWidth);
outerTextParagraph.getLines();
height += (outerTextParagraph != null ? outerTextParagraph.getHeight() + marginBetweenElementsY
: 0);
yStart = writeOrCalculateParagraph(outerTextParagraph, false);
}
}
tableCellContentStream.close();
}
public float getXPosition() {
return xStart;
}
public void setXPosition(float xStart) {
this.xStart = xStart;
}
public float getYPosition() {
return yStart;
}
public void setYPosition(float yStart) {
this.yStart = yStart;
}
@Override
public float getTextHeight() {
return height;
}
@Override
public float getHorizontalFreeSpace() {
return getInnerWidth() - width;
}
@Override
public float getVerticalFreeSpace() {
return getInnerHeight() - width;
}
}

View file

@ -0,0 +1,25 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.font.PDFont;
import java.io.IOException;
class TextToken extends Token {
private PDFont cachedWidthFont;
private float cachedWidth;
TextToken(TokenType type, String data) {
super(type, data);
}
@Override
public float getWidth(PDFont font) throws IOException {
if (font == cachedWidthFont) {
return cachedWidth;
}
cachedWidth = super.getWidth(font);
cachedWidthFont = font;
return cachedWidth;
}
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.layout.boxable;
public enum TextType {
HIGHLIGHT, UNDERLINE, SQUIGGLY, STRIKEOUT
}

View file

@ -0,0 +1,56 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import org.apache.pdfbox.pdmodel.font.PDFont;
import java.io.IOException;
import java.util.Objects;
public class Token {
private final TokenType type;
private final String data;
public Token(TokenType type, String data) {
this.type = type;
this.data = data;
}
public String getData() {
return data;
}
public TokenType getType() {
return type;
}
public float getWidth(PDFont font) throws IOException {
return font.getStringWidth(getData());
}
@Override
public String toString() {
return getClass().getSimpleName() + "[" + type + "/" + data + "]";
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Token token = (Token) o;
return getType() == token.getType() &&
Objects.equals(getData(), token.getData());
}
@Override
public int hashCode() {
return Objects.hash(getType(), getData());
}
public static Token text(TokenType type, String data) {
return new TextToken(type, data);
}
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.layout.boxable;
public enum TokenType {
TEXT, POSSIBLE_WRAP_POINT, WRAP_POINT, OPEN_TAG, CLOSE_TAG, PADDING, BULLET, ORDERING
}

View file

@ -0,0 +1,304 @@
package org.xbib.graphics.pdfbox.layout.boxable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
public final class Tokenizer {
private static final Token OPEN_TAG_I = new Token(TokenType.OPEN_TAG, "i");
private static final Token OPEN_TAG_B = new Token(TokenType.OPEN_TAG, "b");
private static final Token OPEN_TAG_OL = new Token(TokenType.OPEN_TAG, "ol");
private static final Token OPEN_TAG_UL = new Token(TokenType.OPEN_TAG, "ul");
private static final Token CLOSE_TAG_I = new Token(TokenType.CLOSE_TAG, "i");
private static final Token CLOSE_TAG_B = new Token(TokenType.CLOSE_TAG, "b");
private static final Token CLOSE_TAG_OL = new Token(TokenType.CLOSE_TAG, "ol");
private static final Token CLOSE_TAG_UL = new Token(TokenType.CLOSE_TAG, "ul");
private static final Token CLOSE_TAG_P = new Token(TokenType.CLOSE_TAG, "p");
private static final Token CLOSE_TAG_LI = new Token(TokenType.CLOSE_TAG, "li");
private static final Token POSSIBLE_WRAP_POINT = new Token(TokenType.POSSIBLE_WRAP_POINT, "");
private static final Token WRAP_POINT_P = new Token(TokenType.WRAP_POINT, "p");
private static final Token WRAP_POINT_LI = new Token(TokenType.WRAP_POINT, "li");
private static final Token WRAP_POINT_BR = new Token(TokenType.WRAP_POINT, "br");
private Tokenizer() {
}
private static boolean isWrapPointChar(char ch) {
return
ch == ' ' ||
ch == ',' ||
ch == '.' ||
ch == '-' ||
ch == '@' ||
ch == ':' ||
ch == ';' ||
ch == '\n' ||
ch == '\t' ||
ch == '\r' ||
ch == '\f' ||
ch == '\u000B';
}
private static Stack<Integer> findWrapPoints(String text) {
Stack<Integer> result = new Stack<>();
result.push(text.length());
for (int i = text.length() - 2; i >= 0; i--) {
if (isWrapPointChar(text.charAt(i))) {
result.push(i + 1);
}
}
return result;
}
private static Stack<Integer> findWrapPointsWithFunction(String text, WrappingFunction wrappingFunction) {
final String[] split = wrappingFunction.getLines(text);
int textIndex = text.length();
final Stack<Integer> possibleWrapPoints = new Stack<>();
possibleWrapPoints.push(textIndex);
for (int i = split.length - 1; i > 0; i--) {
final int splitLength = split[i].length();
possibleWrapPoints.push(textIndex - splitLength);
textIndex -= splitLength;
}
return possibleWrapPoints;
}
public static List<Token> tokenize(final String text, final WrappingFunction wrappingFunction) {
final List<Token> tokens = new ArrayList<>();
if (text != null) {
final Stack<Integer> possibleWrapPoints = wrappingFunction == null
? findWrapPoints(text)
: findWrapPointsWithFunction(text, wrappingFunction);
int textIndex = 0;
final StringBuilder sb = new StringBuilder();
// taking first wrap point
Integer currentWrapPoint = possibleWrapPoints.pop();
while (textIndex < text.length()) {
if (textIndex == currentWrapPoint) {
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
sb.delete(0, sb.length());
}
tokens.add(POSSIBLE_WRAP_POINT);
currentWrapPoint = possibleWrapPoints.pop();
}
final char c = text.charAt(textIndex);
if (c == '<') {
boolean consumed = false;
if (textIndex < text.length() - 2) {
final char lookahead1 = text.charAt(textIndex + 1);
final char lookahead2 = text.charAt(textIndex + 2);
if ('i' == lookahead1 && '>' == lookahead2) {
// <i>
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(OPEN_TAG_I);
textIndex += 2;
consumed = true;
} else if ('b' == lookahead1 && '>' == lookahead2) {
// <b>
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(OPEN_TAG_B);
textIndex += 2;
consumed = true;
} else if ('b' == lookahead1 && 'r' == lookahead2) {
if (textIndex < text.length() - 3) {
// <br>
final char lookahead3 = text.charAt(textIndex + 3);
if (lookahead3 == '>') {
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(WRAP_POINT_BR);
// normal notation <br>
textIndex += 3;
consumed = true;
} else if (textIndex < text.length() - 4) {
// <br/>
final char lookahead4 = text.charAt(textIndex + 4);
if (lookahead3 == '/' && lookahead4 == '>') {
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(WRAP_POINT_BR);
// normal notation <br/>
textIndex += 4;
consumed = true;
} else if (textIndex < text.length() - 5) {
final char lookahead5 = text.charAt(textIndex + 5);
if (lookahead3 == ' ' && lookahead4 == '/' && lookahead5 == '>') {
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(WRAP_POINT_BR);
// in case it is notation <br />
textIndex += 5;
consumed = true;
}
}
}
}
} else if ('p' == lookahead1 && '>' == lookahead2) {
// <p>
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(WRAP_POINT_P);
textIndex += 2;
consumed = true;
} else if ('o' == lookahead1 && 'l' == lookahead2) {
// <ol>
if (textIndex < text.length() - 3) {
final char lookahead3 = text.charAt(textIndex + 3);
if (lookahead3 == '>') {
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(OPEN_TAG_OL);
textIndex += 3;
consumed = true;
}
}
} else if ('u' == lookahead1 && 'l' == lookahead2) {
// <ul>
if (textIndex < text.length() - 3) {
final char lookahead3 = text.charAt(textIndex + 3);
if (lookahead3 == '>') {
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(OPEN_TAG_UL);
textIndex += 3;
consumed = true;
}
}
} else if ('l' == lookahead1 && 'i' == lookahead2) {
// <li>
if (textIndex < text.length() - 3) {
final char lookahead3 = text.charAt(textIndex + 3);
if (lookahead3 == '>') {
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
// clean string builder
sb.delete(0, sb.length());
}
tokens.add(WRAP_POINT_LI);
textIndex += 3;
consumed = true;
}
}
} else if ('/' == lookahead1) {
// one character tags
if (textIndex < text.length() - 3) {
final char lookahead3 = text.charAt(textIndex + 3);
if ('>' == lookahead3) {
if ('i' == lookahead2) {
// </i>
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
sb.delete(0, sb.length());
}
tokens.add(CLOSE_TAG_I);
textIndex += 3;
consumed = true;
} else if ('b' == lookahead2) {
// </b>
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
sb.delete(0, sb.length());
}
tokens.add(CLOSE_TAG_B);
textIndex += 3;
consumed = true;
} else if ('p' == lookahead2) {
//</p>
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
sb.delete(0, sb.length());
}
tokens.add(CLOSE_TAG_P);
textIndex += 3;
consumed = true;
}
}
}
if (textIndex < text.length() - 4) {
// lists
final char lookahead3 = text.charAt(textIndex + 3);
final char lookahead4 = text.charAt(textIndex + 4);
if ('l' == lookahead3) {
if ('o' == lookahead2 && '>' == lookahead4) {
// </ol>
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
sb.delete(0, sb.length());
}
tokens.add(CLOSE_TAG_OL);
textIndex += 4;
consumed = true;
} else if ('u' == lookahead2 && '>' == lookahead4) {
// </ul>
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
sb.delete(0, sb.length());
}
tokens.add(CLOSE_TAG_UL);
textIndex += 4;
consumed = true;
}
} else if ('l' == lookahead2 && 'i' == lookahead3) {
// </li>
if ('>' == lookahead4) {
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
sb.delete(0, sb.length());
}
tokens.add(CLOSE_TAG_LI);
textIndex += 4;
consumed = true;
}
}
}
}
}
if (!consumed) {
sb.append('<');
}
} else {
sb.append(c);
}
textIndex++;
}
if (sb.length() > 0) {
tokens.add(Token.text(TokenType.TEXT, sb.toString()));
sb.delete(0, sb.length());
}
tokens.add(POSSIBLE_WRAP_POINT);
return tokens;
} else {
return Collections.emptyList();
}
}
}

View file

@ -0,0 +1,18 @@
package org.xbib.graphics.pdfbox.layout.boxable;
public enum VerticalAlignment {
TOP, MIDDLE, BOTTOM;
public static VerticalAlignment get(final String key) {
switch (key == null ? "top" : key.toLowerCase().trim()) {
case "top":
return TOP;
case "middle":
return MIDDLE;
case "bottom":
return BOTTOM;
default:
return TOP;
}
}
}

View file

@ -0,0 +1,6 @@
package org.xbib.graphics.pdfbox.layout.boxable;
public interface WrappingFunction {
String[] getLines(String text);
}

View file

@ -93,7 +93,7 @@ public class Document implements RenderListener {
private Entry<Element, LayoutHint> createEntry(final Element element,
final LayoutHint layoutHint) {
return new SimpleEntry<Element, LayoutHint>(element, layoutHint);
return new SimpleEntry<>(element, layoutHint);
}
/**
@ -237,31 +237,20 @@ public class Document implements RenderListener {
Element element = entry.getKey();
LayoutHint layoutHint = entry.getValue();
boolean success = false;
// first ask custom renderer to render the element
Iterator<Renderer> customRendererIterator = customRenderer
.iterator();
Iterator<Renderer> customRendererIterator = customRenderer.iterator();
while (!success && customRendererIterator.hasNext()) {
success = customRendererIterator.next().render(renderContext,
element, layoutHint);
success = customRendererIterator.next().render(renderContext, element, layoutHint);
}
// if none of them felt responsible, let the default renderer do the job.
if (!success) {
success = renderContext.render(renderContext, element,
layoutHint);
success = renderContext.render(renderContext, element, layoutHint);
}
if (!success) {
throw new IllegalArgumentException(
String.format(
throw new IllegalArgumentException(String.format(
"neither layout %s nor the render context knows what to do with %s",
renderContext.getLayout(), element));
}
}
renderContext.close();
resetPDDocument();
return document;
}

View file

@ -10,9 +10,9 @@ import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.WidthRespecting;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* The frame is a container for a {@link Drawable}, that allows to add margin,
@ -23,7 +23,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
*/
public class Frame implements Element, Drawable, WidthRespecting, Dividable {
private final List<Drawable> innerList = new CopyOnWriteArrayList<>();
private final List<Drawable> innerList = new ArrayList<>();
private float paddingLeft;
private float paddingRight;

View file

@ -8,7 +8,6 @@ import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.TextFlow;
import org.xbib.graphics.pdfbox.layout.text.TextSequenceUtil;
import org.xbib.graphics.pdfbox.layout.text.WidthRespecting;
import java.io.IOException;
/**
* A paragraph is used as a container for {@link TextFlow text} that is drawn as
@ -54,23 +53,22 @@ public class Paragraph extends TextFlow implements Drawable, Element, WidthRespe
@Override
public void draw(PDDocument pdDocument, PDPageContentStream contentStream,
Position upperLeft, DrawListener drawListener) throws IOException {
Position upperLeft, DrawListener drawListener) {
drawText(contentStream, upperLeft, getAlignment(), drawListener);
}
@Override
public Divided divide(float remainingHeight, final float pageHeight)
throws IOException {
public Divided divide(float remainingHeight, final float pageHeight) {
return TextSequenceUtil.divide(this, getMaxWidth(), remainingHeight);
}
@Override
public Paragraph removeLeadingEmptyVerticalSpace() throws IOException {
public Paragraph removeLeadingEmptyVerticalSpace() {
return removeLeadingEmptyLines();
}
@Override
public Paragraph removeLeadingEmptyLines() throws IOException {
public Paragraph removeLeadingEmptyLines() {
Paragraph result = (Paragraph) super.removeLeadingEmptyLines();
result.setAbsolutePosition(this.getAbsolutePosition());
result.setAlignment(this.getAlignment());

View file

@ -4,6 +4,7 @@ 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.apache.pdfbox.util.Matrix;
import org.xbib.graphics.pdfbox.layout.elements.ControlElement;
import org.xbib.graphics.pdfbox.layout.elements.Document;
import org.xbib.graphics.pdfbox.layout.elements.Element;
@ -17,7 +18,6 @@ import org.xbib.graphics.pdfbox.layout.text.DrawContext;
import org.xbib.graphics.pdfbox.layout.text.DrawListener;
import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationDrawListener;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.io.Closeable;
import java.io.IOException;
@ -204,8 +204,7 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList
* @return <code>true</code> if the page is rotated by 90/270 degrees.
*/
public boolean isPageTilted() {
return CompatibilityHelper.getPageRotation(page) == 90
|| CompatibilityHelper.getPageRotation(page) == 270;
return page.getRotation() == 90 || page.getRotation() == 270;
}
/**
@ -374,12 +373,9 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList
if (nextPageFormat != null) {
setPageFormat(nextPageFormat);
}
this.page = new PDPage(getPageFormat().getMediaBox());
this.pdDocument.addPage(page);
this.contentStream = CompatibilityHelper
.createAppendablePDPageContentStream(pdDocument, page);
this.contentStream = new PDPageContentStream(pdDocument, page, PDPageContentStream.AppendMode.APPEND, true);
// fix orientation
if (getPageOrientation() != getPageFormat().getOrientation()) {
if (isPageTilted()) {
@ -389,7 +385,7 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList
}
}
if (isPageTilted()) {
CompatibilityHelper.transform(contentStream, 0, 1, -1, 0, getPageHeight(), 0);
contentStream.transform(new Matrix(0, 1, -1, 0, getPageHeight(), 0));
}
resetPositionToUpperLeft();
resetMaxPositionOnPage();
@ -410,8 +406,7 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList
document.afterPage(this);
if (getPageFormat().getRotation() != 0) {
int currentRotation = CompatibilityHelper
.getPageRotation(getCurrentPage());
int currentRotation = getCurrentPage().getRotation();
getCurrentPage().setRotation(
currentRotation + getPageFormat().getRotation());
}

View file

@ -11,7 +11,6 @@ import org.xbib.graphics.pdfbox.layout.elements.VerticalSpacer;
import org.xbib.graphics.pdfbox.layout.text.Alignment;
import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.WidthRespecting;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.io.IOException;
/**
@ -246,17 +245,13 @@ public class VerticalLayout implements Layout {
break;
}
}
contentStream.saveGraphicsState();
contentStream.addRect(0, pageFormat.getMarginBottom(), renderContext.getPageWidth(),
renderContext.getHeight());
CompatibilityHelper.clip(contentStream);
contentStream.clip();
drawable.draw(renderContext.getPdDocument(), contentStream,
renderContext.getCurrentPosition().add(offsetX, 0), renderContext);
contentStream.restoreGraphicsState();
if (movePosition) {
renderContext.movePositionBy(0, -drawable.getHeight());
}

View file

@ -12,10 +12,10 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font;
public enum BaseFont implements Font {
TIMES(PDType1Font.TIMES_ROMAN, PDType1Font.TIMES_BOLD,
PDType1Font.TIMES_ITALIC, PDType1Font.TIMES_BOLD_ITALIC), //
PDType1Font.TIMES_ITALIC, PDType1Font.TIMES_BOLD_ITALIC),
COURIER(PDType1Font.COURIER, PDType1Font.COURIER_BOLD,
PDType1Font.COURIER_OBLIQUE, PDType1Font.COURIER_BOLD_OBLIQUE), //
PDType1Font.COURIER_OBLIQUE, PDType1Font.COURIER_BOLD_OBLIQUE),
HELVETICA(PDType1Font.HELVETICA, PDType1Font.HELVETICA_BOLD,
PDType1Font.HELVETICA_OBLIQUE, PDType1Font.HELVETICA_BOLD_OBLIQUE);
@ -28,8 +28,7 @@ public enum BaseFont implements Font {
private final PDFont boldItalicFont;
BaseFont(PDFont plainFont, PDFont boldFont, PDFont italicFont,
PDFont boldItalicFont) {
BaseFont(PDFont plainFont, PDFont boldFont, PDFont italicFont, PDFont boldItalicFont) {
this.plainFont = plainFont;
this.boldFont = boldFont;
this.italicFont = italicFont;
@ -37,7 +36,7 @@ public enum BaseFont implements Font {
}
@Override
public PDFont getPlainFont() {
public PDFont getRegularFont() {
return plainFont;
}

View file

@ -4,7 +4,7 @@ import org.apache.pdfbox.pdmodel.font.PDFont;
public interface Font {
PDFont getPlainFont();
PDFont getRegularFont();
PDFont getBoldFont();

View file

@ -1,47 +1,56 @@
package org.xbib.graphics.pdfbox.layout.font;
import org.apache.pdfbox.pdmodel.font.PDFont;
import java.util.Objects;
/**
* Container for a Font and size.
*/
public class FontDescriptor {
/**
* the associated font.
*/
private final PDFont font;
private final Font font;
/**
* the font size.
*/
private final float size;
/**
* Creates the descriptor the the given font and size.
*
* @param font the font.
* @param size the size.
*/
public FontDescriptor(final PDFont font, final float size) {
this.font = font;
this.size = size;
private final boolean regular;
private final boolean bold;
private final boolean italic;
public FontDescriptor(Font font, float size) {
this(font, size, false, false);
}
/**
* @return the font.
*/
public PDFont getFont() {
public FontDescriptor(Font font, float size, boolean bold, boolean italic) {
this.font = font;
this.size = size;
this.regular = !bold && !italic;
this.bold = bold;
this.italic = italic;
}
public Font getFont() {
return font;
}
/**
* @return the size.
*/
public float getSize() {
return size;
}
public PDFont getSelectedFont() {
if (regular) {
return font.getRegularFont();
}
if (italic) {
return bold ? font.getBoldItalicFont() : font.getItalicFont();
}
if (bold) {
return font.getBoldFont();
}
throw new IllegalStateException();
}
@Override
public String toString() {
return "FontDescriptor [font=" + font + ", size=" + size + "]";
@ -49,33 +58,14 @@ public class FontDescriptor {
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((font == null) ? 0 : font.hashCode());
result = prime * result + Float.floatToIntBits(size);
return result;
return Objects.hash(font, size, regular, bold, italic);
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
if (!(obj instanceof FontDescriptor)) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
return Objects.hashCode(obj) == Objects.hash(this);
}
final FontDescriptor other = (FontDescriptor) obj;
if (font == null) {
if (other.font != null) {
return false;
}
} else if (!font.equals(other.font)) {
return false;
}
return Float.floatToIntBits(size) == Float.floatToIntBits(other.size);
}
}

View file

@ -4,10 +4,13 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects;
public class NotoSansFont implements Font {
private final PDDocument document;
private static PDType0Font regular;
private static PDType0Font bold;
@ -16,32 +19,47 @@ public class NotoSansFont implements Font {
private static PDType0Font bolditalic;
public NotoSansFont(PDDocument document) throws IOException {
if (regular == null) {
regular = PDType0Font.load(document, Objects.requireNonNull(getClass().getResourceAsStream("NotoSans-Regular.ttf")));
bold = PDType0Font.load(document, Objects.requireNonNull(getClass().getResourceAsStream("NotoSans-Bold.ttf")));
italic = PDType0Font.load(document, Objects.requireNonNull(getClass().getResourceAsStream("NotoSans-Italic.ttf")));
bolditalic = PDType0Font.load(document, Objects.requireNonNull(getClass().getResourceAsStream("NotoSans-BoldItalic.ttf")));
}
public NotoSansFont(PDDocument document) {
this.document = document;
}
@Override
public PDFont getPlainFont() {
public PDFont getRegularFont() {
if (regular == null) {
regular = load(document, "NotoSans-Regular.ttf");
}
return regular;
}
@Override
public PDFont getBoldFont() {
if (bold == null) {
bold = load(document, "NotoSans-Bold.ttf");
}
return bold;
}
@Override
public PDFont getItalicFont() {
if (italic == null) {
italic = load(document, "NotoSans-Italic.ttf");
}
return italic;
}
@Override
public PDFont getBoldItalicFont() {
if (bolditalic == null) {
bolditalic = load(document, "NotoSans-BoldItalic.ttf");
}
return bolditalic;
}
private static PDType0Font load(PDDocument document, String resourceName) {
try {
return PDType0Font.load(document, Objects.requireNonNull(NotoSansFont.class.getResourceAsStream(resourceName)));
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

View file

@ -4,16 +4,13 @@ import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.xbib.graphics.pdfbox.layout.text.DrawListener;
import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.awt.Color;
import java.io.IOException;
/**
* Abstract base class for shapes which performs the
* {@link #fill(PDDocument, PDPageContentStream, Position, float, float, Color, DrawListener)}
* and (@link
* {@link #draw(PDDocument, PDPageContentStream, Position, float, float, Color, Stroke, DrawListener)}
* .
* and {@link #draw(PDDocument, PDPageContentStream, Position, float, float, Color, Stroke, DrawListener)}
*/
public abstract class AbstractShape implements Shape {
@ -21,9 +18,7 @@ public abstract class AbstractShape implements Shape {
public void draw(PDDocument pdDocument, PDPageContentStream contentStream,
Position upperLeft, float width, float height, Color color,
Stroke stroke, DrawListener drawListener) throws IOException {
add(pdDocument, contentStream, upperLeft, width, height);
if (stroke != null) {
stroke.applyTo(contentStream);
}
@ -31,29 +26,22 @@ public abstract class AbstractShape implements Shape {
contentStream.setStrokingColor(color);
}
contentStream.stroke();
if (drawListener != null) {
drawListener.drawn(this, upperLeft, width, height);
}
}
@Override
public void fill(PDDocument pdDocument, PDPageContentStream contentStream,
Position upperLeft, float width, float height, Color color,
DrawListener drawListener) throws IOException {
add(pdDocument, contentStream, upperLeft, width, height);
if (color != null) {
contentStream.setNonStrokingColor(color);
}
CompatibilityHelper.fillNonZero(contentStream);
contentStream.fill();
if (drawListener != null) {
drawListener.drawn(this, upperLeft, width, height);
}
}
}

View file

@ -3,7 +3,6 @@ package org.xbib.graphics.pdfbox.layout.shape;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.io.IOException;
/**
@ -14,6 +13,7 @@ public class RoundRect extends AbstractShape {
private final static float BEZ = 0.551915024494f;
private final float cornerRadiusX;
private final float cornerRadiusY;
/**
@ -96,18 +96,16 @@ public class RoundRect extends AbstractShape {
contentStream.moveTo(a.getX(), a.getY());
addLine(contentStream, a.getX(), a.getY(), b.getX(), b.getY());
CompatibilityHelper.curveTo(contentStream, b.getX() + bezX, b.getY(), c.getX(),
contentStream.curveTo(b.getX() + bezX, b.getY(), c.getX(),
c.getY() + bezY, c.getX(), c.getY());
// contentStream.addLine(c.getX(), c.getY(), d.getX(), d.getY());
addLine(contentStream, c.getX(), c.getY(), d.getX(), d.getY());
CompatibilityHelper.curveTo(contentStream, d.getX(), d.getY() - bezY, e.getX() + bezX,
contentStream.curveTo(d.getX(), d.getY() - bezY, e.getX() + bezX,
e.getY(), e.getX(), e.getY());
// contentStream.addLine(e.getX(), e.getY(), f.getX(), f.getY());
addLine(contentStream, e.getX(), e.getY(), f.getX(), f.getY());
CompatibilityHelper.curveTo(contentStream, f.getX() - bezX, f.getY(), g.getX(),
contentStream.curveTo(f.getX() - bezX, f.getY(), g.getX(),
g.getY() - bezY, g.getX(), g.getY());
addLine(contentStream, g.getX(), g.getY(), h.getX(), h.getY());
CompatibilityHelper.curveTo(contentStream, h.getX(), h.getY() + bezY, a.getX() - bezX,
contentStream.curveTo(h.getX(), h.getY() + bezY, a.getX() - bezX,
a.getY(), a.getX(), a.getY());
}
@ -120,7 +118,7 @@ public class RoundRect extends AbstractShape {
float y1, float x2, float y2) throws IOException {
float xMid = (x1 + x2) / 2f;
float yMid = (y1 + y2) / 2f;
CompatibilityHelper.curveTo1(contentStream, xMid, yMid, x2, y2);
contentStream.curveTo1(xMid, yMid, x2, y2);
}
}

View file

@ -2,6 +2,8 @@ package org.xbib.graphics.pdfbox.layout.shape;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.logging.Logger;
/**
* This is a container for all information needed to perform a stroke.
@ -51,6 +53,7 @@ public class Stroke {
public static class DashPattern {
private final float[] pattern;
private final float phase;
/**
@ -106,8 +109,11 @@ public class Stroke {
}
private final CapStyle capStyle;
private final JoinStyle joinStyle;
private final DashPattern dashPattern;
private final float lineWidth;
/**
@ -168,18 +174,20 @@ public class Stroke {
* @param contentStream the content stream to apply this stroke to.
* @throws IOException by PDFBox.
*/
public void applyTo(final PDPageContentStream contentStream)
throws IOException {
public void applyTo(PDPageContentStream contentStream) throws IOException {
if (getCapStyle() != null) {
Logger.getLogger("").info(" cap style = " + getCapStyle().value());
contentStream.setLineCapStyle(getCapStyle().value());
}
if (getJoinStyle() != null) {
Logger.getLogger("").info(" join style = " + getJoinStyle().value());
contentStream.setLineJoinStyle(getJoinStyle().value());
}
if (getDashPattern() != null) {
contentStream.setLineDashPattern(getDashPattern().getPattern(),
getDashPattern().getPhase());
Logger.getLogger("").info(" dash pattern = " + Arrays.asList(getDashPattern().getPattern()));
contentStream.setLineDashPattern(getDashPattern().getPattern(), getDashPattern().getPhase());
}
Logger.getLogger("").info(" line width = " + getLineWidth());
contentStream.setLineWidth(getLineWidth());
}
@ -196,10 +204,14 @@ public class Stroke {
* A builder providing a fluent interface for creating a stroke.
*/
public static class StrokeBuilder {
private CapStyle capStyle = CapStyle.Cap;
private JoinStyle joinStyle = JoinStyle.Miter;
private DashPattern dashPattern;
float lineWidth = 1f;
private float lineWidth = 1f;
public StrokeBuilder capStyle(CapStyle capStyle) {
this.capStyle = capStyle;

View file

@ -0,0 +1,329 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.table.render.AbstractCellRenderer;
import org.xbib.graphics.pdfbox.layout.table.render.Renderer;
import java.awt.Color;
public abstract class AbstractCell {
private static final float DEFAULT_MIN_HEIGHT = 10f;
private int colSpan = 1;
private int rowSpan = 1;
protected AbstractCellRenderer<AbstractCell> drawer;
private Row row;
private Column column;
private float width;
private float minHeight = DEFAULT_MIN_HEIGHT;
protected Settings settings;
public void setColSpan(int colSpan) {
this.colSpan = colSpan;
}
public void setRowSpan(int rowSpan) {
this.rowSpan = rowSpan;
}
public float getPaddingBottom() {
return settings.getPaddingBottom();
}
public float getPaddingTop() {
return settings.getPaddingTop();
}
public float getPaddingLeft() {
return settings.getPaddingLeft();
}
public float getPaddingRight() {
return settings.getPaddingRight();
}
public float getHorizontalPadding() {
return settings.getPaddingLeft() + settings.getPaddingRight();
}
public float getVerticalPadding() {
return settings.getPaddingTop() + settings.getPaddingBottom();
}
public float getBorderWidthTop() {
return hasBorderTop() ? settings.getBorderWidthTop() : 0;
}
public boolean hasBorderTop() {
return settings.getBorderWidthTop() != null && settings.getBorderWidthTop() > 0;
}
public float getBorderWidthBottom() {
return hasBorderBottom() ? settings.getBorderWidthBottom() : 0;
}
public boolean hasBorderBottom() {
return settings.getBorderWidthBottom() != null && settings.getBorderWidthBottom() > 0;
}
public float getBorderWidthLeft() {
return hasBorderLeft() ? settings.getBorderWidthLeft() : 0;
}
public boolean hasBorderLeft() {
return settings.getBorderWidthLeft() != null && settings.getBorderWidthLeft() > 0;
}
public float getBorderWidthRight() {
return hasBorderRight() ? settings.getBorderWidthRight() : 0;
}
public boolean hasBorderRight() {
return settings.getBorderWidthRight() != null && settings.getBorderWidthRight() > 0;
}
public BorderStyleInterface getBorderStyleTop() {
return settings.getBorderStyleTop();
}
public BorderStyleInterface getBorderStyleBottom() {
return settings.getBorderStyleBottom();
}
public BorderStyleInterface getBorderStyleLeft() {
return settings.getBorderStyleLeft();
}
public BorderStyleInterface getBorderStyleRight() {
return settings.getBorderStyleRight();
}
public boolean hasBackgroundColor() {
return settings.getBackgroundColor() != null;
}
public Color getBackgroundColor() {
return settings.getBackgroundColor();
}
public Color getBorderColor() {
return settings.getBorderColor();
}
public boolean isWordBreak() {
return settings.isWordBreak();
}
public Column getColumn() {
return column;
}
public static float getDefaultMinHeight() {
return DEFAULT_MIN_HEIGHT;
}
public float getMinHeight() {
return minHeight;
}
public float getWidth() {
return width;
}
public int getColSpan() {
return colSpan;
}
public int getRowSpan() {
return rowSpan;
}
public Row getRow() {
return row;
}
public Settings getSettings() {
return settings;
}
public void setColumn(Column column) {
this.column = column;
}
public void setDrawer(AbstractCellRenderer<AbstractCell> drawer) {
this.drawer = drawer;
}
public void setMinHeight(float minHeight) {
this.minHeight = minHeight;
}
public void setRow(Row row) {
this.row = row;
}
public void setSettings(Settings settings) {
this.settings = settings;
}
public void setWidth(float width) {
this.width = width;
}
public float getHeight() {
assertIsRendered();
return getRowSpan() > 1 ? calculateHeightForRowSpan() : getMinHeight();
}
public Renderer getDrawer() {
return this.drawer != null ? this.drawer.withCell(this) : createDefaultDrawer();
}
protected abstract Renderer createDefaultDrawer();
public float calculateHeightForRowSpan() {
Row currentRow = row;
float result = currentRow.getHeight();
for (int i = 1; i < getRowSpan(); i++) {
result += currentRow.getNext().getHeight();
currentRow = currentRow.getNext();
}
return result;
}
protected void assertIsRendered() {
if (column == null || row == null) {
throw new TableNotYetBuiltException();
}
}
public boolean isHorizontallyAligned(HorizontalAlignment alignment) {
return getSettings().getHorizontalAlignment() == alignment;
}
public boolean isVerticallyAligned(VerticalAlignment alignment) {
return getSettings().getVerticalAlignment() == alignment;
}
/*public abstract static class AbstractCellBuilder<C extends AbstractCell, B extends AbstractCell.AbstractCellBuilder<C, B>> {
protected Settings settings = new Settings();
// We don't want to expose settings directly!
private void settings(Settings settings) {}
public B borderWidth(final float borderWidth) {
settings.setBorderWidthTop(borderWidth);
settings.setBorderWidthBottom(borderWidth);
settings.setBorderWidthLeft(borderWidth);
settings.setBorderWidthRight(borderWidth);
return (B) this;
}
public B borderWidthTop(final float borderWidth) {
settings.setBorderWidthTop(borderWidth);
return (B) this;
}
public B borderWidthBottom(final float borderWidth) {
settings.setBorderWidthBottom(borderWidth);
return (B) this;
}
public B borderWidthLeft(final float borderWidth) {
settings.setBorderWidthLeft(borderWidth);
return (B) this;
}
public B borderWidthRight(final float borderWidth) {
settings.setBorderWidthRight(borderWidth);
return (B) this;
}
public B borderStyleTop(final BorderStyleInterface style) {
settings.setBorderStyleTop(style);
return (B) this;
}
public B borderStyleBottom(final BorderStyleInterface style) {
settings.setBorderStyleBottom(style);
return (B) this;
}
public B borderStyleLeft(final BorderStyleInterface style) {
settings.setBorderStyleLeft(style);
return (B) this;
}
public B borderStyleRight(final BorderStyleInterface style) {
settings.setBorderStyleRight(style);
return (B) this;
}
public B borderStyle(final BorderStyleInterface style) {
return this.borderStyleLeft(style)
.borderStyleRight(style)
.borderStyleBottom(style)
.borderStyleTop(style);
}
public B padding(final float padding) {
return this.paddingTop(padding)
.paddingBottom(padding)
.paddingLeft(padding)
.paddingRight(padding);
}
public B paddingTop(final float padding) {
settings.setPaddingTop(padding);
return (B) this;
}
public B paddingBottom(final float padding) {
settings.setPaddingBottom(padding);
return (B) this;
}
public B paddingLeft(final float padding) {
settings.setPaddingLeft(padding);
return (B) this;
}
public B paddingRight(final float padding) {
settings.setPaddingRight(padding);
return (B) this;
}
public B horizontalAlignment(final HorizontalAlignment alignment) {
settings.setHorizontalAlignment(alignment);
return (B) this;
}
public B verticalAlignment(final VerticalAlignment alignment) {
settings.setVerticalAlignment(alignment);
return (B) this;
}
public B backgroundColor(final Color backgroundColor) {
settings.setBackgroundColor(backgroundColor);
return (B) this;
}
public B borderColor(final Color borderColor) {
settings.setBorderColor(borderColor);
return (B) this;
}
public B wordBreak(final Boolean wordBreak) {
settings.setWordBreak(wordBreak);
return (B) this;
}
}*/
}

View file

@ -0,0 +1,100 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.font.Font;
import java.awt.Color;
import java.util.Comparator;
import java.util.List;
public abstract class AbstractTextCell extends AbstractCell {
protected float lineSpacing = 1f;
public Font getFont() {
return settings.getFont();
}
public Integer getFontSize() {
return settings.getFontSize();
}
public Color getTextColor() {
return settings.getTextColor();
}
private Float textHeight;
public abstract String getText();
public float getLineSpacing() {
return lineSpacing;
}
@Override
public float getMinHeight() {
return Math.max((getVerticalPadding() + getTextHeight()), super.getMinHeight());
}
public float getTextHeight() {
if (this.textHeight != null) {
return this.textHeight;
}
this.textHeight = PdfUtil.getFontHeight(getFont(), getFontSize());
if (settings.isWordBreak()) {
final int size = PdfUtil.getOptimalTextBreakLines(getText(), getFont(), getFontSize(), getMaxWidth()).size();
final float heightOfTextLines = size * this.textHeight;
final float heightOfLineSpacing = (size - 1) * this.textHeight * getLineSpacing();
this.textHeight = heightOfTextLines + heightOfLineSpacing;
}
return this.textHeight;
}
public float getWidthOfText() {
assertIsRendered();
final float notBrokenTextWidth = PdfUtil.getStringWidth(getText(), getFont(), getFontSize());
if (settings.isWordBreak()) {
final float maxWidth = getMaxWidthOfText() - getHorizontalPadding();
List<String> textLines = PdfUtil.getOptimalTextBreakLines(getText(), getFont(), getFontSize(), maxWidth);
return textLines.stream()
.map(line -> PdfUtil.getStringWidth(line, getFont(), getFontSize()))
.max(Comparator.naturalOrder())
.orElse(notBrokenTextWidth);
}
return notBrokenTextWidth;
}
private float getMaxWidthOfText() {
float columnsWidth = getColumn().getWidth();
if (getColSpan() > 1) {
Column currentColumn = getColumn();
for (int i = 1; i < getColSpan(); i++) {
columnsWidth += currentColumn.getNext().getWidth();
currentColumn = currentColumn.getNext();
}
}
return columnsWidth;
}
public float getMaxWidth() {
return getMaxWidthOfText() - getHorizontalPadding();
}
/*public abstract static class AbstractTextCellBuilder<C extends AbstractTextCell, B extends AbstractTextCell.AbstractTextCellBuilder<C, B>> extends AbstractCellBuilder<C, B> {
public B font(final Font font) {
settings.setFont(font);
return (B) this;
}
public B fontSize(final Integer fontSize) {
settings.setFontSize(fontSize);
return (B) this;
}
public B textColor(final Color textColor) {
settings.setTextColor(textColor);
return (B) this;
}
}*/
}

View file

@ -0,0 +1,25 @@
package org.xbib.graphics.pdfbox.layout.table;
public enum BorderStyle implements BorderStyleInterface {
SOLID(new float[]{}, 0),
DOTTED(new float[]{1}, 1),
DASHED(new float[]{5,2}, 1);
private final float[] pattern;
private final int phase;
BorderStyle(float[] pattern, int phase) {
this.pattern = pattern;
this.phase = phase;
}
public float[] getPattern() {
return pattern;
}
public int getPhase() {
return phase;
}
}

View file

@ -0,0 +1,10 @@
package org.xbib.graphics.pdfbox.layout.table;
public interface BorderStyleInterface {
float[] getPattern();
int getPhase();
}

View file

@ -0,0 +1,45 @@
package org.xbib.graphics.pdfbox.layout.table;
public class Column {
private Table table;
private Column next;
private float width;
Column(final float width) {
if (width < 0) {
throw new IllegalArgumentException("Column width must be non-negative");
}
this.width = width;
}
boolean hasNext() {
return next != null;
}
public void setTable(Table table) {
this.table = table;
}
public void setNext(Column next) {
this.next = next;
}
public void setWidth(float width) {
this.width = width;
}
public Table getTable() {
return table;
}
public float getWidth() {
return width;
}
public Column getNext() {
return next;
}
}

View file

@ -0,0 +1,9 @@
package org.xbib.graphics.pdfbox.layout.table;
@SuppressWarnings("serial")
public class CouldNotDetermineStringWidthException extends RuntimeException {
public CouldNotDetermineStringWidthException() {
super();
}
}

View file

@ -0,0 +1,7 @@
package org.xbib.graphics.pdfbox.layout.table;
public enum HorizontalAlignment {
LEFT, CENTER, RIGHT, JUSTIFY
}

View file

@ -0,0 +1,84 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.elements.Paragraph;
import org.xbib.graphics.pdfbox.layout.font.Font;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotatedStyledText;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations;
import java.awt.Color;
import java.util.Collections;
public class Hyperlink implements ParagraphProcessor {
private String text;
private String url;
private Font font;
private Float fontSize;
private Color color = Color.BLUE;
private float baselineOffset = 1f;
public void setFontSize(Float fontSize) {
this.fontSize = fontSize;
}
public void setFont(Font font) {
this.font = font;
}
public Font getFont() {
return font;
}
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
public void setColor(Color color) {
this.color = color;
}
public Color getColor() {
return color;
}
public void setBaselineOffset(float baselineOffset) {
this.baselineOffset = baselineOffset;
}
public float getBaselineOffset() {
return baselineOffset;
}
public void setUrl(String url) {
this.url = url;
}
public String getUrl() {
return url;
}
public Float getFontSize() {
return fontSize;
}
@Override
public void process(Paragraph paragraph, Settings settings) {
Annotations.HyperlinkAnnotation hyperlink =
new Annotations.HyperlinkAnnotation(getUrl(), Annotations.HyperlinkAnnotation.LinkStyle.ul);
FontDescriptor fontDescriptor = new FontDescriptor(getFont() != null ? getFont() : settings.getFont(),
getFontSize() != null ? getFontSize() : settings.getFontSize());
paragraph.add(new AnnotatedStyledText(getText(), fontDescriptor,
getColor(), getBaselineOffset(), 0, 0, Collections.singleton(hyperlink)));
}
}

View file

@ -0,0 +1,92 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.xbib.graphics.pdfbox.layout.table.render.Renderer;
import org.xbib.graphics.pdfbox.layout.table.render.ImageCellRenderer;
import java.awt.geom.Point2D;
public class ImageCell extends AbstractCell {
private float scale = 1.0f;
private PDImageXObject image;
private float maxHeight;
public void setImage(PDImageXObject image) {
this.image = image;
}
public void setMaxHeight(float maxHeight) {
this.maxHeight = maxHeight;
}
public void setScale(float scale) {
this.scale = scale;
}
public float getScale() {
return scale;
}
public PDImageXObject getImage() {
return image;
}
@Override
public float getMinHeight() {
return Math.max((getFitSize().y + getVerticalPadding()), super.getMinHeight());
}
@Override
protected Renderer createDefaultDrawer() {
return new ImageCellRenderer(this);
}
public Point2D.Float getFitSize() {
final Point2D.Float sizes = new Point2D.Float();
float scaledWidth = image.getWidth() * getScale();
float scaledHeight = image.getHeight() * getScale();
final float resultingWidth = getWidth() - getHorizontalPadding();
// maybe reduce the image to fit in column
if (scaledWidth > resultingWidth) {
scaledHeight = (resultingWidth / scaledWidth) * scaledHeight;
scaledWidth = resultingWidth;
}
if (maxHeight > 0.0f && scaledHeight > maxHeight) {
scaledWidth = (maxHeight / scaledHeight) * scaledWidth;
scaledHeight = maxHeight;
}
sizes.x = scaledWidth;
sizes.y = scaledHeight;
return sizes;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private PDImageXObject image;
private Builder() {
}
public Builder image(PDImageXObject image) {
this.image = image;
return this;
}
public ImageCell build() {
ImageCell cell = new ImageCell();
cell.setImage(image);
return cell;
}
}
}

View file

@ -0,0 +1,58 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.elements.Paragraph;
import org.xbib.graphics.pdfbox.layout.font.BaseFont;
import java.io.IOException;
import java.util.EnumMap;
import java.util.Map;
public class Markup implements ParagraphProcessor {
public enum MarkupSupportedFont {
TIMES, COURIER, HELVETICA
}
public static final Map<MarkupSupportedFont, BaseFont> FONT_MAP = new EnumMap<>(Markup.MarkupSupportedFont.class);
static {
FONT_MAP.put(Markup.MarkupSupportedFont.HELVETICA, BaseFont.HELVETICA);
FONT_MAP.put(Markup.MarkupSupportedFont.COURIER, BaseFont.COURIER);
FONT_MAP.put(Markup.MarkupSupportedFont.TIMES, BaseFont.TIMES);
}
private String markup;
private MarkupSupportedFont font;
private Float fontSize;
public void setMarkup(String markup) {
this.markup = markup;
}
public String getMarkup() {
return markup;
}
public void setFont(MarkupSupportedFont font) {
this.font = font;
}
public MarkupSupportedFont getFont() {
return font;
}
public void setFontSize(Float fontSize) {
this.fontSize = fontSize;
}
public Float getFontSize() {
return fontSize;
}
@Override
public void process(Paragraph paragraph, Settings settings) throws IOException {
float fontSize = getFontSize() != null ? getFontSize() : settings.getFontSize();
paragraph.addMarkup(getMarkup(), fontSize, FONT_MAP.get(getFont()));
}
}

View file

@ -0,0 +1,23 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.elements.Paragraph;
import org.xbib.graphics.pdfbox.layout.font.Font;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
public class NewLine implements ParagraphProcessor {
private final Font font;
private final float fontSize;
NewLine(Font font, float fontSize) {
this.font = font;
this.fontSize = fontSize;
}
@Override
public void process(Paragraph paragraph, Settings settings) {
paragraph.add(new org.xbib.graphics.pdfbox.layout.text.NewLine(new FontDescriptor(font, fontSize)));
}
}

View file

@ -0,0 +1,134 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.table.render.Renderer;
import org.xbib.graphics.pdfbox.layout.table.render.ParagraphCellRenderer;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
public class ParagraphCell extends AbstractCell {
protected float lineSpacing = 1f;
private Paragraph paragraph;
public void setLineSpacing(float lineSpacing) {
this.lineSpacing = lineSpacing;
}
public float getLineSpacing() {
return lineSpacing;
}
public void setParagraph(Paragraph paragraph) {
this.paragraph = paragraph;
}
public Paragraph getParagraph() {
return paragraph;
}
@Override
public void setWidth(float width) {
super.setWidth(width);
while (getParagraph().getWrappedParagraph().removeLast() != null) {
}
for (ParagraphProcessor p : getParagraph().getProcessables()) {
try {
p.process(getParagraph().getWrappedParagraph(), getSettings());
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
org.xbib.graphics.pdfbox.layout.elements.Paragraph wrappedParagraph = paragraph.getWrappedParagraph();
wrappedParagraph.setLineSpacing(getLineSpacing());
wrappedParagraph.setApplyLineSpacingToFirstLine(false);
wrappedParagraph.setMaxWidth(width - getHorizontalPadding());
}
@Override
protected Renderer createDefaultDrawer() {
return new ParagraphCellRenderer(this);
}
@Override
public float getMinHeight() {
float height = paragraph.getWrappedParagraph().getHeight() + getVerticalPadding();
return Math.max(height, super.getMinHeight());
}
public static class Paragraph {
private final List<ParagraphProcessor> processables;
private final org.xbib.graphics.pdfbox.layout.elements.Paragraph wrappedParagraph =
new org.xbib.graphics.pdfbox.layout.elements.Paragraph();
public Paragraph(List<ParagraphProcessor> processables) {
this.processables = processables;
}
public List<ParagraphProcessor> getProcessables() {
return processables;
}
public org.xbib.graphics.pdfbox.layout.elements.Paragraph getWrappedParagraph() {
return wrappedParagraph;
}
/*public static class ParagraphBuilder {
private final List<ParagraphProcessable> processables = new LinkedList<>();
private ParagraphBuilder() {
}
public ParagraphBuilder append(StyledText styledText) {
processables.add(styledText);
return this;
}
public ParagraphBuilder append(Hyperlink hyperlink) {
processables.add(hyperlink);
return this;
}
public ParagraphBuilder append(Markup markup) {
processables.add(markup);
return this;
}
public ParagraphBuilder appendNewLine(Font font, float fontSize) {
processables.add(new NewLine(font, fontSize));
return this;
}
public Paragraph build() {
return new Paragraph(processables);
}
}
public static ParagraphBuilder builder() {
return new ParagraphBuilder();
}*/
}
/*public abstract static class ParagraphCellBuilder<C extends ParagraphCell, B extends ParagraphCell.ParagraphCellBuilder<C, B>> extends AbstractCellBuilder<C, B> {
public B font(final Font font) {
settings.setFont(font);
return (B) this;
}
public B fontSize(final Integer fontSize) {
settings.setFontSize(fontSize);
return (B) this;
}
public B textColor(final Color textColor) {
settings.setTextColor(textColor);
return (B) this;
}
}*/
}

View file

@ -0,0 +1,10 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.elements.Paragraph;
import java.io.IOException;
public interface ParagraphProcessor {
void process(Paragraph paragraph, Settings settings) throws IOException;
}

View file

@ -0,0 +1,165 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.font.Font;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Stack;
import java.util.stream.Collectors;
/**
* Provides some helping functions.
*/
public final class PdfUtil {
public static final String NEW_LINE_REGEX = "\\r?\\n";
/**
* Computes the width of a String (in points).
*
* @param text Text
* @param font Font of Text
* @param fontSize FontSize of String
* @return Width (in points)
*/
public static float getStringWidth(final String text, final Font font, final int fontSize) {
return Arrays.stream(text.split(NEW_LINE_REGEX))
.max(Comparator.comparing(String::length))
.map(x -> {
try {
return getWidthOfStringWithoutNewlines(x, font, fontSize);
} catch (IOException exception) {
return 0f;
}
})
.orElseThrow(CouldNotDetermineStringWidthException::new);
}
private static float getWidthOfStringWithoutNewlines(String text, Font font, int fontSize) throws IOException {
final List<String> codePointsAsString = text.codePoints()
.mapToObj(codePoint -> new String(new int[]{codePoint}, 0, 1))
.collect(Collectors.toList());
List<Float> widths = new ArrayList<>();
for (final String codepoint : codePointsAsString) {
try {
widths.add(font.getRegularFont().getStringWidth(codepoint) * fontSize / 1000F);
} catch (final IllegalArgumentException | IOException e) {
widths.add(font.getRegularFont().getStringWidth("") * fontSize / 1000F);
}
}
return widths.stream().reduce(0.0f, Float::sum);
}
/**
* Computes the height of a font.
*
* @param font Font
* @param fontSize FontSize
* @return Height of font
*/
public static float getFontHeight(Font font, final int fontSize) {
return font.getRegularFont().getFontDescriptor().getCapHeight() * fontSize / 1000F;
}
/**
* Split a text into multiple lines to prevent a text-overflow.
*
* @param text Text
* @param font Used font
* @param fontSize Used fontSize
* @param maxWidth Maximal width of resulting text-lines
* @return A list of lines, where all are smaller than maxWidth
*/
public static List<String> getOptimalTextBreakLines(final String text, final Font font, final int fontSize, final float maxWidth) {
List<String> result = new ArrayList<>();
for (final String line : text.split(NEW_LINE_REGEX)) {
if (PdfUtil.doesTextLineFit(line, font, fontSize, maxWidth)) {
result.add(line);
} else {
result.addAll(PdfUtil.wrapLine(line, font, fontSize, maxWidth));
}
}
return result;
}
private static List<String> wrapLine(final String line, final Font font, final int fontSize, final float maxWidth) {
if (PdfUtil.doesTextLineFit(line, font, fontSize, maxWidth)) {
return Collections.singletonList(line);
}
List<String> goodLines = new ArrayList<>();
Stack<String> allWords = new Stack<>();
Arrays.asList(line.split("(?<=[\\\\. ,-])")).forEach(allWords::push);
Collections.reverse(allWords);
while (!allWords.empty()) {
goodLines.add(buildALine(allWords, font, fontSize, maxWidth));
}
return goodLines;
}
private static List<String> splitBySize(final String line, final Font font, final int fontSize, final float maxWidth) {
List<String> returnList = new ArrayList<>();
for (int i = line.length() - 1; i > 0; i--) {
String fittedNewLine = line.substring(0, i) + "-";
String remains = line.substring(i);
if (PdfUtil.doesTextLineFit(fittedNewLine, font, fontSize, maxWidth)) {
returnList.add(fittedNewLine);
returnList.addAll(PdfUtil.wrapLine(remains, font, fontSize, maxWidth));
break;
}
}
return returnList;
}
private static String buildALine(final Stack<String> words,
final Font font,
final int fontSize,
final float maxWidth) {
StringBuilder line = new StringBuilder();
float width = 0;
while (!words.empty()) {
float nextWordWidth = getStringWidth(words.peek(), font, fontSize);
if (line.length() == 0 && words.peek().length() == 1 && nextWordWidth > maxWidth) {
return words.pop();
}
if (doesTextLineFit(width + nextWordWidth, maxWidth)) {
line.append(words.pop());
width += nextWordWidth;
} else {
break;
}
}
if (width == 0 && !words.empty()) {
List<String> cutBySize = splitBySize(words.pop(), font, fontSize, maxWidth);
Collections.reverse(cutBySize);
cutBySize.forEach(words::push);
return buildALine(words, font, fontSize, maxWidth);
}
return line.toString().trim();
}
private static boolean doesTextLineFit(String textLine, Font font, int fontSize, float maxWidth) {
return doesTextLineFit(PdfUtil.getStringWidth(textLine, font, fontSize), maxWidth);
}
private static boolean doesTextLineFit(float stringWidth, float maxWidth) {
if (isEqualInEpsilon(stringWidth, maxWidth)) {
return true;
}
return maxWidth > stringWidth;
}
private static boolean isEqualInEpsilon(float x, float y) {
return Math.abs(y - x) < 0.0001;
}
}

View file

@ -0,0 +1,76 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.function.Supplier;
public class RepeatedHeaderTableRenderer extends TableRenderer {
private int numberOfRowsToRepeat = 1;
private Float headerHeight;
public RepeatedHeaderTableRenderer(Table table) {
super(table);
}
@Override
protected void drawPage(PageData pageData) {
if (pageData.firstRowOnPage != 0) {
float adaption = 0;
for (int i = 0; i < numberOfRowsToRepeat; i++) {
adaption += table.getRows().get(i).getHeight();
Point2D.Float startPoint = new Point2D.Float(this.startX,
this.startY + calculateHeightForFirstRows() - adaption);
drawRow(startPoint, table.getRows().get(i), i, (drawer, drawingContext) -> {
drawer.renderBackground(drawingContext);
drawer.renderContent(drawingContext);
drawer.renderBorders(drawingContext);
});
}
}
drawerList.forEach(drawer -> drawWithFunction(pageData, new Point2D.Float(this.startX, this.startY), drawer));
}
@Override
protected void determinePageToStartTable(float yOffsetOnNewPage) {
float minimumRowsToFitHeight = 0;
int minimumRowsToFit = table.getRows().size() > numberOfRowsToRepeat
? numberOfRowsToRepeat + 1
: numberOfRowsToRepeat;
for (final Row row : table.getRows().subList(0, minimumRowsToFit))
minimumRowsToFitHeight += row.getHeight();
if (startY - minimumRowsToFitHeight < endY) {
startY = yOffsetOnNewPage + calculateHeightForFirstRows();
startTableInNewPage = true;
}
}
@Override
public void draw(Supplier<PDDocument> documentSupplier, Supplier<PDPage> pageSupplier, float yOffset)
throws IOException {
super.draw(documentSupplier, pageSupplier, yOffset + calculateHeightForFirstRows());
}
private float calculateHeightForFirstRows() {
if (headerHeight != null) {
return headerHeight;
}
float height = 0;
for (int i = 0; i < numberOfRowsToRepeat; i++) {
height += table.getRows().get(i).getHeight();
}
// Cache and return
headerHeight = height;
return height;
}
}

View file

@ -0,0 +1,180 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.font.Font;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import static java.util.Comparator.naturalOrder;
public class Row {
private static final Float DEFAULT_HEIGHT = 10f;
private Table table;
private List<AbstractCell> cells;
private Settings settings;
private Float height;
private Row next;
private Row(final List<AbstractCell> cells) {
super();
this.cells = cells;
}
public void setSettings(Settings settings) {
this.settings = settings;
}
public Settings getSettings() {
return settings;
}
public Row getNext() {
return next;
}
public static Float getDefaultHeight() {
return DEFAULT_HEIGHT;
}
public List<AbstractCell> getCells() {
return cells;
}
public Table getTable() {
return table;
}
public void setCells(List<AbstractCell> cells) {
this.cells = cells;
}
public void setHeight(Float height) {
this.height = height;
}
public void setNext(Row next) {
this.next = next;
}
public void setTable(Table table) {
this.table = table;
}
public float getHeight() {
if (table == null) {
throw new TableNotYetBuiltException();
}
if (height == null) {
this.height = getCells().stream()
.filter(cell -> cell.getRowSpan() == 1)
.map(AbstractCell::getHeight)
.max(naturalOrder())
.orElse(DEFAULT_HEIGHT);
}
return height;
}
void doRowSpanSizeAdaption(float heightOfHighestCell, float rowsHeight) {
final float rowSpanSizeDifference = heightOfHighestCell - rowsHeight;
this.height += (this.height / (heightOfHighestCell - rowSpanSizeDifference)) * rowSpanSizeDifference;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final List<AbstractCell> cells = new ArrayList<>();
private final Settings settings = new Settings();
private Builder() {
}
public Builder add(final AbstractCell cell) {
cells.add(cell);
return this;
}
public Builder font(Font font) {
settings.setFont(font);
return this;
}
public Builder fontSize(final Integer fontSize) {
settings.setFontSize(fontSize);
return this;
}
public Builder textColor(final Color textColor) {
settings.setTextColor(textColor);
return this;
}
public Builder backgroundColor(final Color backgroundColor) {
settings.setBackgroundColor(backgroundColor);
return this;
}
public Builder padding(final float padding) {
settings.setPaddingTop(padding);
settings.setPaddingBottom(padding);
settings.setPaddingLeft(padding);
settings.setPaddingRight(padding);
return this;
}
public Builder borderWidth(final float borderWidth) {
settings.setBorderWidthTop(borderWidth);
settings.setBorderWidthBottom(borderWidth);
settings.setBorderWidthLeft(borderWidth);
settings.setBorderWidthRight(borderWidth);
return this;
}
public Builder borderStyle(final BorderStyleInterface borderStyle) {
settings.setBorderStyleTop(borderStyle);
settings.setBorderStyleBottom(borderStyle);
settings.setBorderStyleLeft(borderStyle);
settings.setBorderStyleRight(borderStyle);
return this;
}
public Builder borderColor(final Color borderColor) {
settings.setBorderColor(borderColor);
return this;
}
public Builder horizontalAlignment(HorizontalAlignment alignment) {
settings.setHorizontalAlignment(alignment);
return this;
}
public Builder verticalAlignment(VerticalAlignment alignment) {
settings.setVerticalAlignment(alignment);
return this;
}
public Builder wordBreak(Boolean wordBreak) {
settings.setWordBreak(wordBreak);
return this;
}
public Row build() {
final Row row = new Row(cells);
row.setSettings(settings);
//row.setHeight(height);
return row;
}
}
}

View file

@ -0,0 +1,10 @@
package org.xbib.graphics.pdfbox.layout.table;
@SuppressWarnings("serial")
public class RowIsTooHighException extends RuntimeException {
public RowIsTooHighException(String message) {
super(message);
}
}

View file

@ -0,0 +1,321 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.font.Font;
import java.awt.Color;
public class Settings {
private Font font;
private Integer fontSize;
private Color textColor;
private Color backgroundColor;
private Float borderWidthTop;
private Float borderWidthBottom;
private Float borderWidthLeft;
private Float borderWidthRight;
private Color borderColor;
private Float paddingLeft;
private Float paddingRight;
private Float paddingTop;
private Float paddingBottom;
private BorderStyleInterface borderStyleLeft;
private BorderStyleInterface borderStyleRight;
private BorderStyleInterface borderStyleTop;
private BorderStyleInterface borderStyleBottom;
private HorizontalAlignment horizontalAlignment;
private VerticalAlignment verticalAlignment;
// We use a boxed Boolean internally in order to be able to model the absence of a value.
// For callers outside it should expose only the primitive though.
private Boolean wordBreak;
public boolean isWordBreak() {
return wordBreak != null && wordBreak;
}
public void setWordBreak(boolean wordBreak) {
this.wordBreak = wordBreak;
}
public void fillingMergeBy(Settings settings) {
fillingMergeFontSettings(settings);
fillingMergePaddingSettings(settings);
fillingMergeBorderWidthSettings(settings);
fillingMergeBorderStyleSettings(settings);
fillingMergeColorSettings(settings);
fillingMergeAlignmentSettings(settings);
fillingMergeWordBreakSetting(settings);
}
public Boolean getWordBreak() {
return wordBreak;
}
public BorderStyleInterface getBorderStyleBottom() {
return borderStyleBottom;
}
public BorderStyleInterface getBorderStyleLeft() {
return borderStyleLeft;
}
public BorderStyleInterface getBorderStyleRight() {
return borderStyleRight;
}
public BorderStyleInterface getBorderStyleTop() {
return borderStyleTop;
}
public Color getBackgroundColor() {
return backgroundColor;
}
public Color getBorderColor() {
return borderColor;
}
public Color getTextColor() {
return textColor;
}
public Float getBorderWidthBottom() {
return borderWidthBottom;
}
public Float getBorderWidthLeft() {
return borderWidthLeft;
}
public Float getBorderWidthRight() {
return borderWidthRight;
}
public Float getBorderWidthTop() {
return borderWidthTop;
}
public Float getPaddingBottom() {
return paddingBottom;
}
public Float getPaddingLeft() {
return paddingLeft;
}
public Float getPaddingRight() {
return paddingRight;
}
public Float getPaddingTop() {
return paddingTop;
}
public HorizontalAlignment getHorizontalAlignment() {
return horizontalAlignment;
}
public Integer getFontSize() {
return fontSize;
}
public Font getFont() {
return font;
}
public VerticalAlignment getVerticalAlignment() {
return verticalAlignment;
}
public void setBackgroundColor(Color backgroundColor) {
this.backgroundColor = backgroundColor;
}
public void setBorderColor(Color borderColor) {
this.borderColor = borderColor;
}
public void setBorderStyleBottom(BorderStyleInterface borderStyleBottom) {
this.borderStyleBottom = borderStyleBottom;
}
public void setBorderStyleLeft(BorderStyleInterface borderStyleLeft) {
this.borderStyleLeft = borderStyleLeft;
}
public void setBorderStyleRight(BorderStyleInterface borderStyleRight) {
this.borderStyleRight = borderStyleRight;
}
public void setBorderStyleTop(BorderStyleInterface borderStyleTop) {
this.borderStyleTop = borderStyleTop;
}
public void setBorderWidthBottom(Float borderWidthBottom) {
this.borderWidthBottom = borderWidthBottom;
}
public void setBorderWidthLeft(Float borderWidthLeft) {
this.borderWidthLeft = borderWidthLeft;
}
public void setBorderWidthRight(Float borderWidthRight) {
this.borderWidthRight = borderWidthRight;
}
public void setBorderWidthTop(Float borderWidthTop) {
this.borderWidthTop = borderWidthTop;
}
public void setFont(Font font) {
this.font = font;
}
public void setFontSize(Integer fontSize) {
this.fontSize = fontSize;
}
public void setHorizontalAlignment(HorizontalAlignment horizontalAlignment) {
this.horizontalAlignment = horizontalAlignment;
}
public void setPaddingBottom(Float paddingBottom) {
this.paddingBottom = paddingBottom;
}
public void setPaddingLeft(Float paddingLeft) {
this.paddingLeft = paddingLeft;
}
public void setPaddingRight(Float paddingRight) {
this.paddingRight = paddingRight;
}
public void setPaddingTop(Float paddingTop) {
this.paddingTop = paddingTop;
}
public void setTextColor(Color textColor) {
this.textColor = textColor;
}
public void setVerticalAlignment(VerticalAlignment verticalAlignment) {
this.verticalAlignment = verticalAlignment;
}
public void setWordBreak(Boolean wordBreak) {
this.wordBreak = wordBreak;
}
private void fillingMergeWordBreakSetting(Settings settings) {
// Note that we use the boxed Boolean only here internally!
if (wordBreak == null && settings.wordBreak != null) {
wordBreak = settings.getWordBreak();
}
}
private void fillingMergePaddingSettings(Settings settings) {
if (getPaddingBottom() == null && settings.getPaddingBottom() != null) {
paddingBottom = settings.getPaddingBottom();
}
if (getPaddingTop() == null && settings.getPaddingTop() != null) {
paddingTop = settings.getPaddingTop();
}
if (getPaddingLeft() == null && settings.getPaddingLeft() != null) {
paddingLeft = settings.getPaddingLeft();
}
if (getPaddingRight() == null && settings.getPaddingRight() != null) {
paddingRight = settings.getPaddingRight();
}
}
private void fillingMergeBorderWidthSettings(Settings settings) {
if (getBorderWidthBottom() == null && settings.getBorderWidthBottom() != null) {
borderWidthBottom = settings.getBorderWidthBottom();
}
if (getBorderWidthTop() == null && settings.getBorderWidthTop() != null) {
borderWidthTop = settings.getBorderWidthTop();
}
if (getBorderWidthLeft() == null && settings.getBorderWidthLeft() != null) {
borderWidthLeft = settings.getBorderWidthLeft();
}
if (getBorderWidthRight() == null && settings.getBorderWidthRight() != null) {
borderWidthRight = settings.getBorderWidthRight();
}
}
private void fillingMergeBorderStyleSettings(Settings settings) {
if (getBorderStyleBottom() == null && settings.getBorderStyleBottom() != null) {
borderStyleBottom = settings.getBorderStyleBottom();
}
if (getBorderStyleTop() == null && settings.getBorderStyleTop() != null) {
borderStyleTop = settings.getBorderStyleTop();
}
if (getBorderStyleLeft() == null && settings.getBorderStyleLeft() != null) {
borderStyleLeft = settings.getBorderStyleLeft();
}
if (getBorderStyleRight() == null && settings.getBorderStyleRight() != null) {
borderStyleRight = settings.getBorderStyleRight();
}
}
private void fillingMergeColorSettings(Settings settings) {
if (getTextColor() == null && settings.getTextColor() != null) {
textColor = settings.getTextColor();
}
if (getBackgroundColor() == null && settings.getBackgroundColor() != null) {
backgroundColor = settings.getBackgroundColor();
}
if (getBorderColor() == null && settings.getBorderColor() != null) {
borderColor = settings.getBorderColor();
}
}
private void fillingMergeAlignmentSettings(Settings settings) {
if (getHorizontalAlignment() == null && settings.getHorizontalAlignment() != null) {
horizontalAlignment = settings.getHorizontalAlignment();
}
if (getVerticalAlignment() == null && settings.getVerticalAlignment() != null) {
verticalAlignment = settings.getVerticalAlignment();
}
}
private void fillingMergeFontSettings(Settings settings) {
if (getFont() == null && settings.getFont() != null) {
font = settings.getFont();
}
if (getFontSize() == null && settings.getFontSize() != null) {
fontSize = settings.getFontSize();
}
}
}

View file

@ -0,0 +1,66 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.elements.Paragraph;
import org.xbib.graphics.pdfbox.layout.font.Font;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import java.awt.Color;
public class StyledText implements ParagraphProcessor {
private String text;
private Float fontSize;
private Font font;
private Color color;
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
public void setFontSize(Float fontSize) {
this.fontSize = fontSize;
}
public Float getFontSize() {
return fontSize;
}
public void setFont(Font font) {
this.font = font;
}
public Font getFont() {
return font;
}
public void setColor(Color color) {
this.color = color;
}
public Color getColor() {
return color;
}
@Override
public void process(Paragraph paragraph, Settings settings) {
final float actualFontSize = getFontSize() != null ? getFontSize() : settings.getFontSize();
final Font actualFont = getFont() != null ? getFont() : settings.getFont();
final Color actualColor = getColor() != null ? getColor() : settings.getTextColor();
// TODO this is a complete mess to handle new lines!!!
String[] lines = getText().split(PdfUtil.NEW_LINE_REGEX);
for (int i = 0; i < lines.length; i++) {
FontDescriptor fontDescriptor = new FontDescriptor(actualFont, actualFontSize);
paragraph.add(new org.xbib.graphics.pdfbox.layout.text.StyledText(lines[i], fontDescriptor, actualColor, 0f, 0, 0));
if (i < lines.length - 1) {
paragraph.add(new org.xbib.graphics.pdfbox.layout.text.NewLine(new FontDescriptor(actualFont, actualFontSize)));
}
}
}
}

View file

@ -0,0 +1,371 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.font.BaseFont;
import org.xbib.graphics.pdfbox.layout.font.Font;
import java.awt.Color;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public class Table {
private static final Font DEFAULT_FONT = BaseFont.HELVETICA;
private static final int DEFAULT_FONT_SIZE = 12;
private static final Color DEFAULT_TEXT_COLOR = Color.BLACK;
private static final Color DEFAULT_BORDER_COLOR = Color.BLACK;
private static final BorderStyleInterface DEFAULT_BORDER_STYLE = BorderStyle.SOLID;
private static final float DEFAULT_PADDING = 4f;
private static final HorizontalAlignment DEFAULT_HORIZONTAL_ALIGNMENT = HorizontalAlignment.LEFT;
private static final VerticalAlignment DEFAULT_VERTICAL_ALIGNMENT = VerticalAlignment.MIDDLE;
private final List<Row> rows;
private final List<Column> columns;
private final Set<Point> rowSpanCells;
private Settings settings;
private int numberOfColumns;
private float width;
public Table(List<Row> rows, List<Column> columns, Set<Point> rowSpanCells) {
this.rows = rows;
this.columns = columns;
this.rowSpanCells = rowSpanCells;
}
public void setSettings(Settings settings) {
this.settings = settings;
}
public Settings getSettings() {
return settings;
}
public void setWidth(float width) {
this.width = width;
}
public float getWidth() {
return width;
}
public static BorderStyleInterface getDefaultBorderStyle() {
return DEFAULT_BORDER_STYLE;
}
public static Color getDefaultBorderColor() {
return DEFAULT_BORDER_COLOR;
}
public static Color getDefaultTextColor() {
return DEFAULT_TEXT_COLOR;
}
public static float getDefaultPadding() {
return DEFAULT_PADDING;
}
public static HorizontalAlignment getDefaultHorizontalAlignment() {
return DEFAULT_HORIZONTAL_ALIGNMENT;
}
public static int getDefaultFontSize() {
return DEFAULT_FONT_SIZE;
}
public int getNumberOfColumns() {
return numberOfColumns;
}
public List<Column> getColumns() {
return columns;
}
public List<Row> getRows() {
return rows;
}
public static Font getDefaultFont() {
return DEFAULT_FONT;
}
public Set<Point> getRowSpanCells() {
return rowSpanCells;
}
public static VerticalAlignment getDefaultVerticalAlignment() {
return DEFAULT_VERTICAL_ALIGNMENT;
}
public void setNumberOfColumns(int numberOfColumns) {
this.numberOfColumns = numberOfColumns;
}
public float getHeight() {
return rows.stream().map(Row::getHeight).reduce(0F, Float::sum);
}
public boolean isRowSpanAt(int rowIndex, int columnIndex) {
return rowSpanCells.contains(new Point(rowIndex, columnIndex));
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final Settings settings = new Settings();
private final List<Row> rows = new ArrayList<>();
private final List<Column> columns = new ArrayList<>();
private final Set<Point> rowSpanCells = new HashSet<>();
private int numberOfColumns;
private float width;
private Builder() {
settings.setFont(DEFAULT_FONT);
settings.setFontSize(DEFAULT_FONT_SIZE);
settings.setTextColor(DEFAULT_TEXT_COLOR);
settings.setBorderColor(DEFAULT_BORDER_COLOR);
settings.setBorderStyleTop(DEFAULT_BORDER_STYLE);
settings.setBorderStyleBottom(DEFAULT_BORDER_STYLE);
settings.setBorderStyleLeft(DEFAULT_BORDER_STYLE);
settings.setBorderStyleRight(DEFAULT_BORDER_STYLE);
settings.setPaddingTop(DEFAULT_PADDING);
settings.setPaddingBottom(DEFAULT_PADDING);
settings.setPaddingLeft(DEFAULT_PADDING);
settings.setPaddingRight(DEFAULT_PADDING);
settings.setWordBreak(true);
}
public Builder addRow(final Row row) {
// Store how many cells can or better have to be omitted in the next rows
// due to cells in this row that declare row spanning
updateRowSpanCellsSet(row.getCells());
if (!rows.isEmpty()) {
rows.get(rows.size() - 1).setNext(row);
}
rows.add(row);
return this;
}
private float getAvailableCellWidthRespectingSpan(int columnIndex, int span) {
float cellWidth = 0;
for (int i = 0; i < span; i++) {
cellWidth += columns.get(columnIndex + i).getWidth();
}
return cellWidth;
}
// This method is unfortunately a bit complex, but what it does is basically:
// Put every cell coordinate in the set which needs to be skipped because it is
// "contained" in another cell due to row spanning.
// The coordinates are those of the table how it would look like without any spanning.
private void updateRowSpanCellsSet(List<AbstractCell> cells) {
int currentColumn = 0;
for (AbstractCell cell : cells) {
while (rowSpanCells.contains(new Point(rows.size(), currentColumn))) {
currentColumn++;
}
if (cell.getRowSpan() > 1) {
for (int rowsToSpan = 0; rowsToSpan < cell.getRowSpan(); rowsToSpan++) {
// Skip first row's cell, because that is a regular cell
if (rowsToSpan >= 1) {
for (int colSpan = 0; colSpan < cell.getColSpan(); colSpan++) {
rowSpanCells.add(new Point(rows.size() + rowsToSpan, currentColumn + colSpan));
}
}
}
}
currentColumn += cell.getColSpan();
}
}
public Builder addColumnsOfWidth(final float... columnWidths) {
for (float columnWidth : columnWidths) {
addColumnOfWidth(columnWidth);
}
return this;
}
public Builder addColumnOfWidth(final float width) {
Column column = new Column(width);
numberOfColumns++;
columns.add(column);
this.width += column.getWidth();
return this;
}
public Builder font(Font font) {
settings.setFont(font);
return this;
}
public Builder fontSize(final Integer fontSize) {
settings.setFontSize(fontSize);
return this;
}
public Builder textColor(final Color textColor) {
settings.setTextColor(textColor);
return this;
}
public Builder backgroundColor(final Color backgroundColor) {
settings.setBackgroundColor(backgroundColor);
return this;
}
public Builder borderWidth(final float borderWidth) {
settings.setBorderWidthTop(borderWidth);
settings.setBorderWidthBottom(borderWidth);
settings.setBorderWidthLeft(borderWidth);
settings.setBorderWidthRight(borderWidth);
return this;
}
public Builder borderStyle(final BorderStyleInterface borderStyle) {
settings.setBorderStyleTop(borderStyle);
settings.setBorderStyleBottom(borderStyle);
settings.setBorderStyleLeft(borderStyle);
settings.setBorderStyleRight(borderStyle);
return this;
}
public Builder padding(final float padding) {
settings.setPaddingTop(padding);
settings.setPaddingBottom(padding);
settings.setPaddingLeft(padding);
settings.setPaddingRight(padding);
return this;
}
public Builder borderColor(final Color borderColor) {
settings.setBorderColor(borderColor);
return this;
}
public Builder horizontalAlignment(HorizontalAlignment alignment) {
settings.setHorizontalAlignment(alignment);
return this;
}
public Builder verticalAlignment(VerticalAlignment alignment) {
settings.setVerticalAlignment(alignment);
return this;
}
public Builder wordBreak(Boolean wordBreak) {
settings.setWordBreak(wordBreak);
return this;
}
public Table build() {
if (getNumberOfRegularCells() != getNumberOfSpannedCells()) {
throw new TableSetupException("Number of table cells does not match with table setup. " +
"This could be due to row or col spanning not being correct");
}
Table table = new Table(rows, columns, rowSpanCells);
table.setSettings(settings);
table.setWidth(width);
table.setNumberOfColumns(numberOfColumns);
setupConnectionsBetweenElementsFor(table);
correctHeightOfCellsDueToRowSpanningIfNecessaryFor(table);
return table;
}
private void setupConnectionsBetweenElementsFor(Table table) {
for (int rowIndex = 0; rowIndex < rows.size(); rowIndex++) {
Row row = rows.get(rowIndex);
row.setTable(table);
if (table.getSettings() != null) {
row.getSettings().fillingMergeBy(table.getSettings());
}
int columnIndex = 0;
for (AbstractCell cell : row.getCells()) {
cell.getSettings().fillingMergeBy(row.getSettings());
cell.setRow(row);
while (table.isRowSpanAt(rowIndex, columnIndex)) {
columnIndex++;
}
Column column = table.getColumns().get(columnIndex);
cell.setColumn(column);
cell.setWidth(getAvailableCellWidthRespectingSpan(columnIndex, cell.getColSpan()));
columnIndex += cell.getColSpan();
}
}
for (int i = 0; i < table.getColumns().size(); i++) {
final Column column = table.getColumns().get(i);
column.setTable(table);
if (i < table.getColumns().size() - 1) {
column.setNext(table.getColumns().get(i + 1));
}
}
}
private void correctHeightOfCellsDueToRowSpanningIfNecessaryFor(Table table) {
for (int i = 0; i < table.getRows().size(); i++) {
final Optional<AbstractCell> highestSpanningCell = rows.get(i).getCells().stream()
.filter(x -> x.getRowSpan() > 1)
.max(Comparator.comparing(AbstractCell::getMinHeight));
if (highestSpanningCell.isPresent()) {
final float heightOfHighestCell = highestSpanningCell.get().getMinHeight();
float regularHeightOfRows = 0;
for (int j = i; j < i + highestSpanningCell.get().getRowSpan(); j++) {
regularHeightOfRows += rows.get(j).getHeight();
}
if (heightOfHighestCell > regularHeightOfRows) {
for (int k = 0; k < table.getColumns().size(); k++) {
float rowsHeight = 0;
for (int l = i; l < (i + highestSpanningCell.get().getRowSpan()); l++) {
rowsHeight += table.getRows().get(l).getHeight();
}
for (int l = i; l < (i + highestSpanningCell.get().getRowSpan()); l++) {
final Row rowThatNeedsAdaption = table.getRows().get(l);
rowThatNeedsAdaption.doRowSpanSizeAdaption(heightOfHighestCell, rowsHeight);
}
}
}
}
}
}
private int getNumberOfRegularCells() {
return columns.size() * rows.size();
}
private int getNumberOfSpannedCells() {
return rows.stream()
.flatMapToInt(row -> row.getCells().stream().mapToInt(cell -> cell.getRowSpan() * cell.getColSpan()))
.sum();
}
}
}

View file

@ -0,0 +1,5 @@
package org.xbib.graphics.pdfbox.layout.table;
@SuppressWarnings("serial")
public class TableNotYetBuiltException extends RuntimeException {
}

View file

@ -0,0 +1,246 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.xbib.graphics.pdfbox.layout.table.render.Renderer;
import org.xbib.graphics.pdfbox.layout.table.render.RenderContext;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import static org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode.APPEND;
public class TableRenderer {
protected final Table table;
protected PDPageContentStream contentStream;
protected PDPage page;
protected float startX;
protected float startY;
protected float endY;
protected boolean compress;
protected PDPage tableStartPage;
protected boolean startTableInNewPage;
protected final List<BiConsumer<Renderer, RenderContext>> drawerList = new LinkedList<>();
{
this.drawerList.add((drawer, drawingContext) -> {
drawer.renderBackground(drawingContext);
drawer.renderContent(drawingContext);
});
this.drawerList.add(Renderer::renderBorders);
}
public TableRenderer(Table table) {
this.table = table;
}
public void setStartX(float startX) {
this.startX = startX;
}
public void setStartY(float startY) {
this.startY = startY;
}
public void setEndY(float endY) {
this.endY = endY;
}
public void setContentStream(PDPageContentStream contentStream) {
this.contentStream = contentStream;
}
public void setPage(PDPage page) {
this.page = page;
}
public void draw() {
drawPage(new PageData(0, table.getRows().size()));
}
protected void drawPage(PageData pageData) {
drawerList.forEach(drawer -> drawWithFunction(pageData, new Point2D.Float(this.startX, this.startY), drawer));
}
protected Queue<PageData> computeRowsOnPagesWithNewPageStartOf(float yOffsetOnNewPage) {
final Queue<PageData> dataForPages = new LinkedList<>();
float y = startY;
int firstRowOnPage = 0;
int lastRowOnPage = 0;
for (final Row row : table.getRows()) {
if (isRowTooHighToBeDrawnOnPage(row, yOffsetOnNewPage)) {
throw new RowIsTooHighException("There is a row that is too high to be drawn on a single page");
}
if (isNotDrawableOnPage(y, row)) {
dataForPages.add(new PageData(firstRowOnPage, lastRowOnPage));
y = yOffsetOnNewPage;
firstRowOnPage = lastRowOnPage;
}
y -= row.getHeight();
lastRowOnPage++;
}
dataForPages.add(new PageData(firstRowOnPage, lastRowOnPage));
return dataForPages;
}
private boolean isRowTooHighToBeDrawnOnPage(Row row, float yOffsetOnNewPage) {
return row.getHeight() > (yOffsetOnNewPage - endY);
}
protected void determinePageToStartTable(float yOffsetOnNewPage) {
if (startY - table.getRows().get(0).getHeight() < endY) {
startY = yOffsetOnNewPage;
startTableInNewPage = true;
}
}
public void draw(Supplier<PDDocument> documentSupplier, Supplier<PDPage> pageSupplier, float yOffset) throws IOException {
PDDocument document = documentSupplier.get();
float startOnNewPage = pageSupplier.get().getMediaBox().getHeight() - yOffset;
determinePageToStartTable(startOnNewPage);
final Queue<PageData> pageDataQueue = computeRowsOnPagesWithNewPageStartOf(startOnNewPage);
for (int i = 0; !pageDataQueue.isEmpty(); i++) {
final PDPage pageToDrawOn = determinePageToDraw(i, document, pageSupplier);
if ((i == 0 && startTableInNewPage) || i > 0 || document.getNumberOfPages() == 0) {
startTableInNewPage = false;
}
if (i == 0) {
tableStartPage = pageToDrawOn;
}
try (PDPageContentStream newPageContentStream = new PDPageContentStream(document, pageToDrawOn, APPEND, compress)) {
setContentStream(newPageContentStream);
setPage(pageToDrawOn);
drawPage(pageDataQueue.poll());
}
setStartY(pageToDrawOn.getMediaBox().getHeight() - yOffset);
}
}
protected PDPage determinePageToDraw(int index, PDDocument document, Supplier<PDPage> pageSupplier) {
final PDPage pageToDrawOn;
if ((index == 0 && startTableInNewPage) || index > 0 || document.getNumberOfPages() == 0) {
pageToDrawOn = pageSupplier.get();
document.addPage(pageToDrawOn);
} else {
pageToDrawOn = document.getPage(document.getNumberOfPages() - 1);
}
return pageToDrawOn;
}
protected void drawWithFunction(PageData pageData, Point2D.Float startingPoint, BiConsumer<Renderer, RenderContext> consumer) {
float y = startingPoint.y;
for (int rowIndex = pageData.firstRowOnPage; rowIndex < pageData.firstRowOnNextPage; rowIndex++) {
final Row row = table.getRows().get(rowIndex);
y -= row.getHeight();
drawRow(new Point2D.Float(startingPoint.x, y), row, rowIndex, consumer);
}
}
protected void drawRow(Point2D.Float start, Row row, int rowIndex, BiConsumer<Renderer, RenderContext> consumer) {
float x = start.x;
int columnCounter = 0;
for (AbstractCell cell : row.getCells()) {
while (table.isRowSpanAt(rowIndex, columnCounter)) {
x += table.getColumns().get(columnCounter).getWidth();
columnCounter++;
}
consumer.accept(cell.getDrawer(), new RenderContext(contentStream, page, new Point2D.Float(x, start.y)));
x += cell.getWidth();
columnCounter += cell.getColSpan();
}
}
private boolean isNotDrawableOnPage(float startY, Row row) {
return startY - getHighestCellOf(row) < endY;
}
private Float getHighestCellOf(Row row) {
return row.getCells().stream()
.map(AbstractCell::getHeight)
.max(Comparator.naturalOrder())
.orElse(row.getHeight());
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private Table table;
private PDPageContentStream contentStream;
private float startX;
private float startY;
private float endY;
private Builder() {
}
public Builder table(Table table) {
this.table = table;
return this;
}
public Builder contentStream(PDPageContentStream contentStream) {
this.contentStream = contentStream;
return this;
}
public Builder startX(float startX) {
this.startX = startX;
return this;
}
public Builder startY(float startY) {
this.startY = startY;
return this;
}
public Builder endY(float endY) {
this.endY = endY;
return this;
}
public TableRenderer build() {
TableRenderer tableRenderer = new TableRenderer(table);
tableRenderer.setContentStream(contentStream);
tableRenderer.setStartX(startX);
tableRenderer.setStartY(startY);
tableRenderer.setEndY(endY);
return tableRenderer;
}
}
public static class PageData {
public final int firstRowOnPage;
public final int firstRowOnNextPage;
public PageData(int firstRowOnPage, int firstRowOnNextPage) {
this.firstRowOnPage = firstRowOnPage;
this.firstRowOnNextPage = firstRowOnNextPage;
}
}
}

View file

@ -0,0 +1,9 @@
package org.xbib.graphics.pdfbox.layout.table;
@SuppressWarnings("serial")
public class TableSetupException extends RuntimeException {
public TableSetupException(String message) {
super(message);
}
}

View file

@ -0,0 +1,193 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.font.Font;
import org.xbib.graphics.pdfbox.layout.table.render.Renderer;
import org.xbib.graphics.pdfbox.layout.table.render.TextCellRenderer;
import java.awt.Color;
public class TextCell extends AbstractTextCell {
protected String text;
protected Renderer createDefaultDrawer() {
return new TextCellRenderer<>(this);
}
public void setText(String text) {
this.text = text;
}
@Override
public String getText() {
return text;
}
public static Builder builder() {
return new Builder();
}
public static class Builder {
private final Settings settings;
private String text;
private int colSpan;
private int rowSpan;
private Builder() {
settings = new Settings();
}
public Builder text(String text) {
this.text = text;
return this;
}
public Builder font(Font font) {
settings.setFont(font);
return this;
}
public Builder fontSize(Integer fontSize) {
settings.setFontSize(fontSize);
return this;
}
public Builder textColor(Color textColor) {
settings.setTextColor(textColor);
return this;
}
public Builder borderWidth(float borderWidth) {
settings.setBorderWidthTop(borderWidth);
settings.setBorderWidthBottom(borderWidth);
settings.setBorderWidthLeft(borderWidth);
settings.setBorderWidthRight(borderWidth);
return this;
}
public Builder borderWidthTop(final float borderWidth) {
settings.setBorderWidthTop(borderWidth);
return this;
}
public Builder borderWidthBottom(final float borderWidth) {
settings.setBorderWidthBottom(borderWidth);
return this;
}
public Builder borderWidthLeft(final float borderWidth) {
settings.setBorderWidthLeft(borderWidth);
return this;
}
public Builder borderWidthRight(final float borderWidth) {
settings.setBorderWidthRight(borderWidth);
return this;
}
public Builder borderStyleTop(final BorderStyleInterface style) {
settings.setBorderStyleTop(style);
return this;
}
public Builder borderStyleBottom(final BorderStyleInterface style) {
settings.setBorderStyleBottom(style);
return this;
}
public Builder borderStyleLeft(final BorderStyleInterface style) {
settings.setBorderStyleLeft(style);
return this;
}
public Builder borderStyleRight(final BorderStyleInterface style) {
settings.setBorderStyleRight(style);
return this;
}
public Builder borderStyle(final BorderStyleInterface style) {
return this.borderStyleLeft(style)
.borderStyleRight(style)
.borderStyleBottom(style)
.borderStyleTop(style);
}
public Builder padding(final float padding) {
return this.paddingTop(padding)
.paddingBottom(padding)
.paddingLeft(padding)
.paddingRight(padding);
}
public Builder paddingTop(final float padding) {
settings.setPaddingTop(padding);
return this;
}
public Builder paddingBottom(final float padding) {
settings.setPaddingBottom(padding);
return this;
}
public Builder paddingLeft(final float padding) {
settings.setPaddingLeft(padding);
return this;
}
public Builder paddingRight(final float padding) {
settings.setPaddingRight(padding);
return this;
}
public Builder horizontalAlignment(final HorizontalAlignment alignment) {
settings.setHorizontalAlignment(alignment);
return this;
}
public Builder verticalAlignment(final VerticalAlignment alignment) {
settings.setVerticalAlignment(alignment);
return this;
}
public Builder backgroundColor(final Color backgroundColor) {
settings.setBackgroundColor(backgroundColor);
return this;
}
public Builder borderColor(final Color borderColor) {
settings.setBorderColor(borderColor);
return this;
}
public Builder wordBreak(final Boolean wordBreak) {
settings.setWordBreak(wordBreak);
return this;
}
public Builder colSpan(int colSpan) {
this.colSpan = colSpan;
return this;
}
public Builder rowSpan(int rowSpan) {
this.rowSpan = rowSpan;
return this;
}
public TextCell build() {
TextCell cell = new TextCell();
cell.setSettings(settings);
cell.setText(text);
if (colSpan > 0) {
cell.setColSpan(colSpan);
}
if (rowSpan > 0) {
cell.setRowSpan(rowSpan);
}
return cell;
}
}
}

View file

@ -0,0 +1,7 @@
package org.xbib.graphics.pdfbox.layout.table;
public enum VerticalAlignment {
BOTTOM, MIDDLE, TOP
}

View file

@ -0,0 +1,18 @@
package org.xbib.graphics.pdfbox.layout.table;
import org.xbib.graphics.pdfbox.layout.table.render.Renderer;
import org.xbib.graphics.pdfbox.layout.table.render.VerticalTextCellRenderer;
public class VerticalTextCell extends AbstractTextCell {
private String text;
protected Renderer createDefaultDrawer() {
return new VerticalTextCellRenderer(this);
}
@Override
public String getText() {
return text;
}
}

View file

@ -0,0 +1,138 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import static org.xbib.graphics.pdfbox.layout.table.VerticalAlignment.BOTTOM;
import static org.xbib.graphics.pdfbox.layout.table.VerticalAlignment.MIDDLE;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.xbib.graphics.pdfbox.layout.table.AbstractCell;
import java.awt.Color;
import java.awt.geom.Point2D;
public abstract class AbstractCellRenderer<T extends AbstractCell> implements Renderer {
protected T cell;
public AbstractCellRenderer<T> withCell(T cell) {
this.cell = cell;
return this;
}
@Override
public void renderBackground(RenderContext renderContext) {
if (cell.hasBackgroundColor()) {
final PDPageContentStream contentStream = renderContext.getContentStream();
final Point2D.Float start = renderContext.getStartingPoint();
final float rowHeight = cell.getRow().getHeight();
final float height = Math.max(cell.getHeight(), rowHeight);
final float y = rowHeight < cell.getHeight()
? start.y + rowHeight - cell.getHeight()
: start.y;
PositionedRectangle positionedRectangle =
new PositionedRectangle(start.x, y, cell.getWidth(), height, cell.getBackgroundColor());
RenderUtil.drawRectangle(contentStream, positionedRectangle);
}
}
@Override
public abstract void renderContent(RenderContext renderContext);
@Override
public void renderBorders(RenderContext renderContext) {
final Point2D.Float start = renderContext.getStartingPoint();
final PDPageContentStream contentStream = renderContext.getContentStream();
final float cellWidth = cell.getWidth();
final float rowHeight = cell.getRow().getHeight();
final float height = Math.max(cell.getHeight(), rowHeight);
final float sY = rowHeight < cell.getHeight()
? start.y + rowHeight - cell.getHeight()
: start.y;
final Color cellBorderColor = cell.getBorderColor();
final Color rowBorderColor = cell.getRow().getSettings().getBorderColor();
if (cell.hasBorderTop() || cell.hasBorderBottom()) {
final float correctionLeft = cell.getBorderWidthLeft() / 2;
final float correctionRight = cell.getBorderWidthRight() / 2;
if (cell.hasBorderTop()) {
PositionedLine line = new PositionedLine();
line.setStartX(start.x - correctionLeft);
line.setStartY(start.y + rowHeight);
line.setEndX(start.x + cellWidth + correctionRight);
line.setEndY(start.y + rowHeight);
line.setWidth(cell.getBorderWidthTop());
line.setColor(cellBorderColor);
line.setResetColor(rowBorderColor);
line.setBorderStyle(cell.getBorderStyleTop());
RenderUtil.drawLine(contentStream, line);
}
if (cell.hasBorderBottom()) {
PositionedLine line = new PositionedLine();
line.setStartX(start.x - correctionLeft);
line.setStartY(sY);
line.setEndX(start.x + cellWidth + correctionRight);
line.setEndY(sY);
line.setWidth(cell.getBorderWidthBottom());
line.setColor(cellBorderColor);
line.setResetColor(rowBorderColor);
line.setBorderStyle(cell.getBorderStyleBottom());
RenderUtil.drawLine(contentStream, line);
}
}
if (cell.hasBorderLeft() || cell.hasBorderRight()) {
final float correctionTop = cell.getBorderWidthTop() / 2;
final float correctionBottom = cell.getBorderWidthBottom() / 2;
if (cell.hasBorderLeft()) {
PositionedLine line = new PositionedLine();
line.setStartX(start.x);
line.setStartY(sY - correctionBottom);
line.setEndX(start.x);
line.setEndY(sY + height + correctionTop);
line.setWidth(cell.getBorderWidthLeft());
line.setColor(cellBorderColor);
line.setResetColor(rowBorderColor);
line.setBorderStyle(cell.getBorderStyleLeft());
RenderUtil.drawLine(contentStream, line);
}
if (cell.hasBorderRight()) {
PositionedLine line = new PositionedLine();
line.setStartX(start.x + cellWidth);
line.setStartY(sY - correctionBottom);
line.setEndX(start.x + cellWidth);
line.setEndY(sY + height + correctionTop);
line.setWidth(cell.getBorderWidthRight());
line.setColor(cellBorderColor);
line.setResetColor(rowBorderColor);
line.setBorderStyle(cell.getBorderStyleRight());
RenderUtil.drawLine(contentStream, line);
}
}
}
protected boolean rowHeightIsBiggerThanOrEqualToCellHeight() {
return cell.getRow().getHeight() > cell.getHeight() ||
isEqualInEpsilon(cell.getRow().getHeight(), cell.getHeight());
}
protected float getRowSpanAdaption() {
return cell.getRowSpan() > 1 ? cell.calculateHeightForRowSpan() - cell.getRow().getHeight() : 0;
}
protected float calculateOuterHeight() {
return cell.getRowSpan() > 1 ? cell.getHeight() : cell.getRow().getHeight();
}
protected float getAdaptionForVerticalAlignment() {
if (rowHeightIsBiggerThanOrEqualToCellHeight() || cell.getRowSpan() > 1) {
if (cell.isVerticallyAligned(MIDDLE)) {
return (calculateOuterHeight() / 2 + calculateInnerHeight() / 2) - getRowSpanAdaption();
} else if (cell.isVerticallyAligned(BOTTOM)) {
return (calculateInnerHeight() + cell.getPaddingBottom()) - getRowSpanAdaption();
}
}
return cell.getRow().getHeight() - cell.getPaddingTop();
}
protected abstract float calculateInnerHeight();
private static boolean isEqualInEpsilon(float x, float y) {
return Math.abs(y - x) < 0.0001;
}
}

View file

@ -0,0 +1,43 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.xbib.graphics.pdfbox.layout.table.HorizontalAlignment;
import org.xbib.graphics.pdfbox.layout.table.ImageCell;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.io.UncheckedIOException;
public class ImageCellRenderer extends AbstractCellRenderer<ImageCell> {
public ImageCellRenderer(ImageCell cell) {
this.cell = cell;
}
@Override
public void renderContent(RenderContext renderContext) {
final PDPageContentStream contentStream = renderContext.getContentStream();
final float moveX = renderContext.getStartingPoint().x;
final Point2D.Float size = cell.getFitSize();
final Point2D.Float drawAt = new Point2D.Float();
float xOffset = moveX + cell.getPaddingLeft();
if (cell.getSettings().getHorizontalAlignment() == HorizontalAlignment.RIGHT) {
xOffset = moveX + (cell.getWidth() - (size.x + cell.getPaddingRight()));
} else if (cell.getSettings().getHorizontalAlignment() == HorizontalAlignment.CENTER) {
final float diff = (cell.getWidth() - size.x) / 2;
xOffset = moveX + diff;
}
drawAt.x = xOffset;
drawAt.y = renderContext.getStartingPoint().y + getAdaptionForVerticalAlignment() - size.y;
try {
contentStream.drawImage(cell.getImage(), drawAt.x, drawAt.y, size.x, size.y);
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
@Override
protected float calculateInnerHeight() {
return (float) cell.getFitSize().getY();
}
}

View file

@ -0,0 +1,9 @@
package org.xbib.graphics.pdfbox.layout.table.render;
@SuppressWarnings("serial")
public class PageNotSetException extends RuntimeException {
public PageNotSetException(String message) {
super(message);
}
}

View file

@ -0,0 +1,81 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.xbib.graphics.pdfbox.layout.elements.Paragraph;
import org.xbib.graphics.pdfbox.layout.table.HorizontalAlignment;
import org.xbib.graphics.pdfbox.layout.table.ParagraphCell;
import org.xbib.graphics.pdfbox.layout.text.Alignment;
import org.xbib.graphics.pdfbox.layout.text.DrawContext;
import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationDrawListener;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.EnumMap;
import java.util.Map;
public class ParagraphCellRenderer extends AbstractCellRenderer<ParagraphCell> {
private static final Map<HorizontalAlignment, Alignment> ALIGNMENT_MAP = new EnumMap<>(HorizontalAlignment.class);
static {
ALIGNMENT_MAP.put(HorizontalAlignment.LEFT, Alignment.LEFT);
ALIGNMENT_MAP.put(HorizontalAlignment.RIGHT, Alignment.RIGHT);
ALIGNMENT_MAP.put(HorizontalAlignment.CENTER, Alignment.CENTER);
ALIGNMENT_MAP.put(HorizontalAlignment.JUSTIFY, Alignment.JUSTIFY);
}
public ParagraphCellRenderer(ParagraphCell cell) {
this.cell = cell;
}
@Override
public void renderContent(RenderContext renderContext) {
if (renderContext.getPage() == null) {
throw new PageNotSetException("Page is not set in drawing context. Please ensure the page is set on table drawer.");
}
Paragraph paragraph = cell.getParagraph().getWrappedParagraph();
AnnotationDrawListener annotationDrawListener = createAndGetAnnotationDrawListenerWith(renderContext);
float x = renderContext.getStartingPoint().x + cell.getPaddingLeft();
float y = renderContext.getStartingPoint().y + getAdaptionForVerticalAlignment();
paragraph.drawText(renderContext.getContentStream(),
new Position(x, y),
ALIGNMENT_MAP.getOrDefault(cell.getSettings().getHorizontalAlignment(), Alignment.LEFT),
annotationDrawListener
);
annotationDrawListener.afterPage(null);
annotationDrawListener.afterRender();
try {
renderContext.getPage().getAnnotations().forEach(PDAnnotation::constructAppearances);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
protected float calculateInnerHeight() {
return cell.getParagraph().getWrappedParagraph().getHeight();
}
private AnnotationDrawListener createAndGetAnnotationDrawListenerWith(RenderContext renderContext) {
return new AnnotationDrawListener(new DrawContext() {
@Override
public PDDocument getPdDocument() {
return null;
}
@Override
public PDPage getCurrentPage() {
return renderContext.getPage();
}
@Override
public PDPageContentStream getCurrentPageContentStream() {
return renderContext.getContentStream();
}
});
}
}

View file

@ -0,0 +1,87 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import org.xbib.graphics.pdfbox.layout.table.BorderStyleInterface;
import java.awt.Color;
public class PositionedLine {
private float width;
private float startX;
private float startY;
private float endX;
private float endY;
private Color color;
private Color resetColor;
private BorderStyleInterface borderStyle;
public void setWidth(float width) {
this.width = width;
}
public float getWidth() {
return width;
}
public void setStartX(float startX) {
this.startX = startX;
}
public float getStartX() {
return startX;
}
public void setStartY(float startY) {
this.startY = startY;
}
public float getStartY() {
return startY;
}
public void setEndX(float endX) {
this.endX = endX;
}
public float getEndX() {
return endX;
}
public void setEndY(float endY) {
this.endY = endY;
}
public float getEndY() {
return endY;
}
public void setColor(Color color) {
this.color = color;
}
public Color getColor() {
return color;
}
public void setResetColor(Color resetColor) {
this.resetColor = resetColor;
}
public Color getResetColor() {
return resetColor;
}
public void setBorderStyle(BorderStyleInterface borderStyle) {
this.borderStyle = borderStyle;
}
public BorderStyleInterface getBorderStyle() {
return borderStyle;
}
}

View file

@ -0,0 +1,48 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import java.awt.Color;
public class PositionedRectangle {
private final float x;
private final float y;
private final float width;
private final float height;
private final Color color;
public PositionedRectangle(float x,
float y,
float width,
float height,
Color color) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.color = color;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public float getWidth() {
return width;
}
public float getHeight() {
return height;
}
public Color getColor() {
return color;
}
}

View file

@ -0,0 +1,53 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import org.xbib.graphics.pdfbox.layout.font.Font;
import java.awt.Color;
public class PositionedStyledText {
private final float x;
private final float y;
private final String text;
private final Font font;
private final int fontSize;
private final Color color;
public PositionedStyledText(float x, float y, String text, Font font, int fontSize, Color color) {
this.x = x;
this.y = y;
this.text = text;
this.font = font;
this.fontSize = fontSize;
this.color = color;
}
public float getX() {
return x;
}
public float getY() {
return y;
}
public String getText() {
return text;
}
public Font getFont() {
return font;
}
public int getFontSize() {
return fontSize;
}
public Color getColor() {
return color;
}
}

View file

@ -0,0 +1,33 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import java.awt.geom.Point2D;
public class RenderContext {
private final PDPageContentStream contentStream;
private final PDPage page;
private final Point2D.Float startingPoint;
public RenderContext(PDPageContentStream contentStream, PDPage page, Point2D.Float startingPoint) {
this.contentStream = contentStream;
this.page = page;
this.startingPoint = startingPoint;
}
public PDPageContentStream getContentStream() {
return contentStream;
}
public PDPage getPage() {
return page;
}
public Point2D.Float getStartingPoint() {
return startingPoint;
}
}

View file

@ -0,0 +1,55 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import static org.xbib.graphics.pdfbox.layout.table.BorderStyle.SOLID;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import java.awt.Color;
import java.io.IOException;
import java.io.UncheckedIOException;
public class RenderUtil {
private RenderUtil() {
}
public static void drawText(PDPageContentStream contentStream, PositionedStyledText styledText) {
try {
contentStream.beginText();
contentStream.setNonStrokingColor(styledText.getColor());
// TODO select correct font
contentStream.setFont(styledText.getFont().getRegularFont(), styledText.getFontSize());
contentStream.newLineAtOffset(styledText.getX(), styledText.getY());
contentStream.showText(styledText.getText());
contentStream.endText();
contentStream.setCharacterSpacing(0);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static void drawLine(PDPageContentStream contentStream, PositionedLine line) {
try {
contentStream.moveTo(line.getStartX(), line.getStartY());
contentStream.setLineWidth(line.getWidth());
contentStream.lineTo(line.getEndX(), line.getEndY());
contentStream.setStrokingColor(line.getColor());
contentStream.setLineDashPattern(line.getBorderStyle().getPattern(), line.getBorderStyle().getPhase());
contentStream.stroke();
contentStream.setStrokingColor(line.getResetColor());
contentStream.setLineDashPattern(SOLID.getPattern(), SOLID.getPhase());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
public static void drawRectangle(PDPageContentStream contentStream, PositionedRectangle rectangle) {
try {
contentStream.setNonStrokingColor(rectangle.getColor());
contentStream.addRect(rectangle.getX(), rectangle.getY(), rectangle.getWidth(), rectangle.getHeight());
contentStream.fill();
contentStream.setNonStrokingColor(Color.BLACK);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

View file

@ -0,0 +1,11 @@
package org.xbib.graphics.pdfbox.layout.table.render;
public interface Renderer {
void renderContent(RenderContext renderContext);
void renderBackground(RenderContext renderContext);
void renderBorders(RenderContext renderContext);
}

View file

@ -0,0 +1,88 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import static org.xbib.graphics.pdfbox.layout.table.HorizontalAlignment.CENTER;
import static org.xbib.graphics.pdfbox.layout.table.HorizontalAlignment.JUSTIFY;
import static org.xbib.graphics.pdfbox.layout.table.HorizontalAlignment.RIGHT;
import org.xbib.graphics.pdfbox.layout.font.Font;
import org.xbib.graphics.pdfbox.layout.table.AbstractTextCell;
import org.xbib.graphics.pdfbox.layout.table.PdfUtil;
import java.awt.Color;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.List;
public class TextCellRenderer<T extends AbstractTextCell> extends AbstractCellRenderer<AbstractTextCell> {
public TextCellRenderer(T cell) {
this.cell = cell;
}
@Override
public void renderContent(RenderContext renderContext) {
float startX = renderContext.getStartingPoint().x;
Font currentFont = cell.getFont();
int currentFontSize = cell.getFontSize();
Color currentTextColor = cell.getTextColor();
float yOffset = renderContext.getStartingPoint().y + getAdaptionForVerticalAlignment();
float xOffset = startX + cell.getPaddingLeft();
final List<String> lines = calculateAndGetLines(currentFont, currentFontSize, cell.getMaxWidth());
for (int i = 0; i < lines.size(); i++) {
final String line = lines.get(i);
yOffset -= calculateYOffset(currentFont, currentFontSize, i);
final float textWidth = PdfUtil.getStringWidth(line, currentFont, currentFontSize);
if (cell.isHorizontallyAligned(RIGHT)) {
xOffset = startX + (cell.getWidth() - (textWidth + cell.getPaddingRight()));
} else if (cell.isHorizontallyAligned(CENTER)) {
xOffset = startX + (cell.getWidth() - textWidth) / 2;
} else if (cell.isHorizontallyAligned(JUSTIFY) && isNotLastLine(lines, i)) {
try {
renderContext.getContentStream().setCharacterSpacing(calculateCharSpacingFor(line));
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
PositionedStyledText text = new PositionedStyledText(xOffset, yOffset,
line, currentFont, currentFontSize, currentTextColor);
drawText(renderContext, text);
}
}
@Override
protected float calculateInnerHeight() {
return cell.getTextHeight();
}
private float calculateYOffset(Font currentFont, int currentFontSize, int lineIndex) {
return PdfUtil.getFontHeight(currentFont, currentFontSize) +
(lineIndex > 0 ? PdfUtil.getFontHeight(currentFont, currentFontSize) * cell.getLineSpacing() : 0f);
}
static boolean isNotLastLine(List<String> lines, int i) {
return i != lines.size() - 1;
}
protected float calculateCharSpacingFor(String line) {
float charSpacing = 0;
if (line.length() > 1) {
float size = PdfUtil.getStringWidth(line, cell.getFont(), cell.getFontSize());
float free = cell.getWidthOfText() - size;
if (free > 0) {
charSpacing = free / (line.length() - 1);
}
}
return charSpacing;
}
protected List<String> calculateAndGetLines(Font currentFont, int currentFontSize, float maxWidth) {
return cell.isWordBreak()
? PdfUtil.getOptimalTextBreakLines(cell.getText(), currentFont, currentFontSize, maxWidth)
: Collections.singletonList(cell.getText());
}
protected void drawText(RenderContext renderContext, PositionedStyledText positionedStyledText) {
RenderUtil.drawText(renderContext.getContentStream(), positionedStyledText);
}
}

View file

@ -0,0 +1,78 @@
package org.xbib.graphics.pdfbox.layout.table.render;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.util.Matrix;
import org.xbib.graphics.pdfbox.layout.font.Font;
import org.xbib.graphics.pdfbox.layout.table.PdfUtil;
import org.xbib.graphics.pdfbox.layout.table.VerticalTextCell;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.List;
/**
* Allows vertical text drawing. Note that this class is still not fully
* developed, e.g. there is no support for text alignment settings yet.
*/
public class VerticalTextCellRenderer extends AbstractCellRenderer<VerticalTextCell> {
public VerticalTextCellRenderer(VerticalTextCell cell) {
this.cell = cell;
}
@Override
public void renderContent(RenderContext renderContext) {
final float startX = renderContext.getStartingPoint().x;
final float startY = renderContext.getStartingPoint().y;
final Font currentFont = cell.getFont();
final int currentFontSize = cell.getFontSize();
final Color currentTextColor = cell.getTextColor();
float yOffset = startY + cell.getPaddingBottom();
float height = cell.getRow().getHeight();
if (cell.getRowSpan() > 1) {
float rowSpanAdaption = cell.calculateHeightForRowSpan() - cell.getRow().getHeight();
yOffset -= rowSpanAdaption;
height = cell.calculateHeightForRowSpan();
}
final List<String> lines = cell.isWordBreak()
? PdfUtil.getOptimalTextBreakLines(cell.getText(), currentFont, currentFontSize, (height - cell.getVerticalPadding()))
: Collections.singletonList(cell.getText());
float xOffset = startX + cell.getPaddingLeft() - PdfUtil.getFontHeight(currentFont, currentFontSize);
for (int i = 0; i < lines.size(); i++) {
final String line = lines.get(i);
xOffset += (PdfUtil.getFontHeight(currentFont, currentFontSize) + (i > 0 ? PdfUtil.getFontHeight(currentFont, currentFontSize) * cell.getLineSpacing() : 0f));
drawText(line, currentFont, currentFontSize, currentTextColor, xOffset, yOffset, renderContext.getContentStream());
}
}
// TODO this is currently not used
@Override
protected float calculateInnerHeight() {
return 0;
}
protected void drawText(String text, Font font, int fontSize, Color color, float x, float y, PDPageContentStream contentStream) {
try {
// Rotate by 90 degrees counter clockwise
AffineTransform transform = AffineTransform.getTranslateInstance(x, y);
transform.concatenate(AffineTransform.getRotateInstance(Math.PI * 0.5));
transform.concatenate(AffineTransform.getTranslateInstance(-x, -y - fontSize));
contentStream.moveTo(x, y);
contentStream.beginText();
contentStream.setTextMatrix(new Matrix(transform));
contentStream.setNonStrokingColor(color);
// TODO select correct font
contentStream.setFont(font.getRegularFont(), fontSize);
contentStream.newLineAtOffset(x, y);
contentStream.showText(text);
contentStream.endText();
contentStream.setCharacterSpacing(0);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}

View file

@ -1,7 +1,5 @@
package org.xbib.graphics.pdfbox.layout.text;
import java.io.IOException;
/**
* Defines an area with a width and height.
*/
@ -9,13 +7,11 @@ public interface Area {
/**
* @return the width of the area.
* @throws IOException by pdfbox
*/
float getWidth() throws IOException;
float getWidth();
/**
* @return the height of the area.
* @throws IOException by pdfbox
*/
float getHeight() throws IOException;
float getHeight();
}

View file

@ -1,9 +1,8 @@
package org.xbib.graphics.pdfbox.layout.text;
import org.apache.pdfbox.pdmodel.font.PDType1Font;
import org.xbib.graphics.pdfbox.layout.font.BaseFont;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import java.awt.Color;
import java.io.IOException;
/**
* A control fragment has no drawable representation but is meant to control the
@ -11,21 +10,23 @@ import java.io.IOException;
*/
public class ControlFragment implements TextFragment {
protected final static FontDescriptor DEFAULT_FONT_DESCRIPTOR = new FontDescriptor(
PDType1Font.HELVETICA, 11);
protected final static FontDescriptor DEFAULT_FONT_DESCRIPTOR =
new FontDescriptor(BaseFont.HELVETICA, 11);
private String name;
private final String text;
private final FontDescriptor fontDescriptor;
private final Color color;
protected ControlFragment(final String text,
final FontDescriptor fontDescriptor) {
protected ControlFragment(String text, FontDescriptor fontDescriptor) {
this(null, text, fontDescriptor, Color.black);
}
protected ControlFragment(final String name, final String text,
final FontDescriptor fontDescriptor, final Color color) {
protected ControlFragment(String name, final String text,
FontDescriptor fontDescriptor, final Color color) {
this.name = name;
if (this.name == null) {
this.name = getClass().getSimpleName();
@ -36,12 +37,12 @@ public class ControlFragment implements TextFragment {
}
@Override
public float getWidth() throws IOException {
public float getWidth() {
return 0;
}
@Override
public float getHeight() throws IOException {
public float getHeight() {
return getFontDescriptor() == null ? 0 : getFontDescriptor().getSize();
}

View file

@ -1,9 +1,9 @@
package org.xbib.graphics.pdfbox.layout.text;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import java.awt.Color;
import java.io.IOException;
import java.io.UncheckedIOException;
/**
* Control fragment that represents a indent in text.
@ -24,12 +24,9 @@ public class Indent extends ControlFragment {
*
* @param indentWidth the indentation.
* @param indentUnit the indentation unit.
* @throws IOException by pdfbox
*/
public Indent(final float indentWidth, final SpaceUnit indentUnit)
throws IOException {
this("", indentWidth, indentUnit, DEFAULT_FONT_DESCRIPTOR,
Alignment.LEFT, Color.black);
public Indent(float indentWidth, SpaceUnit indentUnit) {
this("", indentWidth, indentUnit, DEFAULT_FONT_DESCRIPTOR, Alignment.LEFT, Color.black);
}
/**
@ -40,16 +37,10 @@ public class Indent extends ControlFragment {
* @param label the label of the indentation.
* @param indentWidth the indentation.
* @param indentUnit the indentation unit.
* @param fontSize the font size, resp. the height of the new line.
* @param font the font to use.
* @throws IOException by pdfbox
* @param descriptor the font size, resp. the height of the new line, the font to use.
*/
public Indent(final String label, final float indentWidth,
final SpaceUnit indentUnit, final float fontSize, final PDFont font)
throws IOException {
this(label, indentWidth, indentUnit, fontSize, font, Alignment.LEFT,
Color.black);
public Indent(String label, float indentWidth, SpaceUnit indentUnit, FontDescriptor descriptor) {
this(label, indentWidth, indentUnit, descriptor, Alignment.LEFT, Color.black);
}
/**
@ -60,38 +51,15 @@ public class Indent extends ControlFragment {
* @param label the label of the indentation.
* @param indentWidth the indentation.
* @param indentUnit the indentation unit.
* @param fontSize the font size, resp. the height of the new line.
* @param font the font to use.
* @param fontDescriptor the font size, resp. the height of the new line, the font to use.
* @param alignment the alignment of the label.
* @throws IOException by pdfbox
*/
public Indent(final String label, final float indentWidth,
final SpaceUnit indentUnit, final float fontSize,
final PDFont font, final Alignment alignment) throws IOException {
this(label, indentWidth, indentUnit, fontSize, font, alignment,
Color.black);
}
/**
* Creates a new line with the
* {@link ControlFragment#DEFAULT_FONT_DESCRIPTOR}'s font and the given
* height.
*
* @param label the label of the indentation.
* @param indentWidth the indentation.
* @param indentUnit the indentation unit.
* @param fontSize the font size, resp. the height of the new line.
* @param font the font to use.
* @param alignment the alignment of the label.
* @param color the color to use.
* @throws IOException by pdfbox
*/
public Indent(final String label, final float indentWidth,
final SpaceUnit indentUnit, final float fontSize,
final PDFont font, final Alignment alignment, final Color color)
throws IOException {
this(label, indentWidth, indentUnit,
new FontDescriptor(font, fontSize), alignment, color);
public Indent(String label,
float indentWidth,
SpaceUnit indentUnit,
FontDescriptor fontDescriptor,
Alignment alignment) {
this(label, indentWidth, indentUnit, fontDescriptor, alignment, Color.black);
}
/**
@ -103,18 +71,19 @@ public class Indent extends ControlFragment {
* @param fontDescriptor the font and size associated with this new line.
* @param alignment the alignment of the label.
* @param color the color to use.
* @throws IOException by pdfbox
*/
public Indent(final String label, final float indentWidth,
final SpaceUnit indentUnit, final FontDescriptor fontDescriptor,
final Alignment alignment, final Color color) throws IOException {
public Indent(String label,
float indentWidth,
SpaceUnit indentUnit,
FontDescriptor fontDescriptor,
Alignment alignment,
Color color) {
super("INDENT", label, fontDescriptor, color);
try {
float indent = calculateIndent(indentWidth, indentUnit, fontDescriptor);
float textWidth = 0;
if (label != null && !label.isEmpty()) {
textWidth = fontDescriptor.getSize()
* fontDescriptor.getFont().getStringWidth(label) / 1000f;
textWidth = fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getStringWidth(label) / 1000f;
}
float marginLeft = 0;
float marginRight = 0;
@ -132,8 +101,10 @@ public class Indent extends ControlFragment {
break;
}
}
styledText = new StyledText(label, getFontDescriptor(), getColor(), 0,
marginLeft, marginRight);
styledText = new StyledText(label, fontDescriptor, getColor(), 0, marginLeft, marginRight);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
@ -147,8 +118,7 @@ public class Indent extends ControlFragment {
indentPt, 0);
}
private float calculateIndent(final float indentWidth,
final SpaceUnit indentUnit, final FontDescriptor fontDescriptor)
private float calculateIndent(float indentWidth, SpaceUnit indentUnit, final FontDescriptor fontDescriptor)
throws IOException {
if (indentWidth < 0) {
return 0;
@ -157,13 +127,10 @@ public class Indent extends ControlFragment {
}
@Override
public float getWidth() throws IOException {
public float getWidth() {
return styledText.getWidth();
}
/**
* @return a styled text representation of the indent.
*/
public StyledText toStyledText() {
return styledText;
}
@ -172,5 +139,4 @@ public class Indent extends ControlFragment {
public String toString() {
return "ControlFragment [" + getName() + ", " + styledText + "]";
}
}

View file

@ -1,11 +1,9 @@
package org.xbib.graphics.pdfbox.layout.text;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import org.xbib.graphics.pdfbox.layout.util.Enumerator;
import org.xbib.graphics.pdfbox.layout.util.EnumeratorFactory;
import java.awt.Color;
import java.io.IOException;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -23,8 +21,7 @@ public class IndentCharacters {
/**
* Represent un-indentation, means effectively indent of 0.
*/
public static IndentCharacter UNINDENT_CHARACTER = new IndentCharacter("0",
"0", "pt");
public static IndentCharacter UNINDENT_CHARACTER = new IndentCharacter("0", "0", "pt");
/**
* An <code>--{7em}</code> indicates an indentation of 7 characters in markup,
@ -38,8 +35,9 @@ public class IndentCharacters {
protected float indentWidth = 4;
protected SpaceUnit indentUnit = SpaceUnit.em;
public IndentCharacter(final String level, final String indentWidth,
final String indentUnit) {
public IndentCharacter(String level,
String indentWidth,
String indentUnit) {
super("INDENT", IndentCharacterFactory.TO_ESCAPE);
try {
this.level = level == null ? 0 : level.length() + 1;
@ -78,16 +76,12 @@ public class IndentCharacters {
* Creates the actual {@link Indent} fragment from this control
* character.
*
* @param fontSize the current font size.
* @param font the current font.
* @param descriptor the current font size, the current font.
* @param color the color to use.
* @return the new Indent.
* @throws IOException by pdfbox
*/
public Indent createNewIndent(final float fontSize, final PDFont font,
final Color color) throws IOException {
return new Indent(nextLabel(), level * indentWidth, indentUnit,
fontSize, font, Alignment.RIGHT, color);
public Indent createNewIndent(FontDescriptor descriptor, Color color) {
return new Indent(nextLabel(), level * indentWidth, indentUnit, descriptor, Alignment.RIGHT, color);
}
@Override
@ -130,7 +124,7 @@ public class IndentCharacters {
* markup, using <code>--</code> as the bullet. The number, the unit, bullet
* character and the brackets are optional. Default indentation is 4
* characters, default unit is <code>em</code> and the default bullet
* depends on {@link CompatibilityHelper#getBulletCharacter(int)}. It can be
* depends on {@link #getBulletCharacter(int)}. It can be
* escaped with a backslash ('\').
*/
public static class ListCharacter extends IndentCharacter {
@ -146,7 +140,7 @@ public class IndentCharacters {
label += " ";
}
} else {
label = CompatibilityHelper.getBulletCharacter(getLevel()) + " ";
label = getBulletCharacter(getLevel()) + " ";
}
}
@ -316,6 +310,17 @@ public class IndentCharacters {
public boolean patternMatchesBeginOfLine() {
return true;
}
}
private static String getBulletCharacter(final int level) {
if (level % 2 == 1) {
return System.getProperty("pdfbox.layout.bullet.odd", BULLET);
}
return System.getProperty("pdfbox.layout.bullet.even", DOUBLE_ANGLE);
}
private static final String BULLET = "\u2022";
private static final String DOUBLE_ANGLE = "\u00bb";
}

View file

@ -8,32 +8,12 @@ import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
*/
public class NewLine extends ControlFragment {
/**
* Creates a new line with the
* {@link ControlFragment#DEFAULT_FONT_DESCRIPTOR}.
*/
public NewLine() {
this(DEFAULT_FONT_DESCRIPTOR);
}
/**
* Creates a new line with the
* {@link ControlFragment#DEFAULT_FONT_DESCRIPTOR}'s font and the given
* height.
*
* @param fontSize the font size, resp. the height of the new line.
*/
public NewLine(final float fontSize) {
this(new FontDescriptor(DEFAULT_FONT_DESCRIPTOR.getFont(), fontSize));
}
/**
* Creates a new line with the given font descriptor.
*
* @param fontDescriptor the font and size associated with this new line.
*/
public NewLine(final FontDescriptor fontDescriptor) {
public NewLine(FontDescriptor fontDescriptor) {
super("\n", fontDescriptor);
}
}

View file

@ -25,10 +25,9 @@ public enum SpaceUnit {
* @return the size in pt.
* @throws IOException by pdfbox
*/
public float toPt(final float size, final FontDescriptor fontDescriptor) throws IOException {
public float toPt(float size, FontDescriptor fontDescriptor) throws IOException {
if (this == em) {
return fontDescriptor.getSize()
* fontDescriptor.getFont().getAverageFontWidth() / 1000 * size;
return fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getAverageFontWidth() / 1000 * size;
}
return size;
}

View file

@ -1,9 +1,9 @@
package org.xbib.graphics.pdfbox.layout.text;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import java.awt.Color;
import java.io.IOException;
import java.io.UncheckedIOException;
/**
* Base class representing drawable text styled with font, size, color etc.
@ -27,51 +27,13 @@ public class StyledText implements TextFragment {
*/
private Float width = null;
/**
* Creates a styled text.
*
* @param text the text to draw. Must not contain line feeds ('\n').
* @param size the size of the font.
* @param font the font to use.
*/
public StyledText(final String text, final float size, final PDFont font) {
this(text, size, font, Color.black);
}
/**
* Creates a styled text.
*
* @param text the text to draw. Must not contain line feeds ('\n').
* @param size the size of the font.
* @param font the font to use.
* @param color the color to use.
*/
public StyledText(final String text, final float size, final PDFont font,
final Color color) {
this(text, new FontDescriptor(font, size), color);
}
/**
* Creates a styled text.
*
* @param text the text to draw. Must not contain line feeds ('\n').
* @param size the size of the font.
* @param font the font to use.
* @param color the color to use.
* @param baselineOffset the offset of the baseline.
*/
public StyledText(final String text, final float size, final PDFont font,
final Color color, final float baselineOffset) {
this(text, new FontDescriptor(font, size), color, baselineOffset, 0, 0);
}
/**
* Creates a styled text.
*
* @param text the text to draw. Must not contain line feeds ('\n').
* @param fontDescriptor the font to use.
*/
public StyledText(final String text, final FontDescriptor fontDescriptor) {
public StyledText(String text, FontDescriptor fontDescriptor) {
this(text, fontDescriptor, Color.black);
}
@ -82,8 +44,7 @@ public class StyledText implements TextFragment {
* @param fontDescriptor the font to use.
* @param color the color to use.
*/
public StyledText(final String text, final FontDescriptor fontDescriptor,
final Color color) {
public StyledText(String text, FontDescriptor fontDescriptor, Color color) {
this(text, fontDescriptor, color, 0, 0, 0);
}
@ -97,12 +58,12 @@ public class StyledText implements TextFragment {
* @param leftMargin the margin left to the text.
* @param rightMargin the margin right to the text.
*/
public StyledText(final String text, final FontDescriptor fontDescriptor,
final Color color, final float baselineOffset,
final float leftMargin, final float rightMargin) {
public StyledText(String text,
FontDescriptor fontDescriptor,
Color color,
float baselineOffset, float leftMargin, float rightMargin) {
if (text.contains("\n")) {
throw new IllegalArgumentException(
"StyledText must not contain line breaks, use TextFragment.LINEBREAK for that");
throw new IllegalArgumentException("StyledText must not contain line breaks, use TextFragment.LINEBREAK for that");
}
if (leftMargin < 0) {
throw new IllegalArgumentException("leftMargin must be >= 0");
@ -133,11 +94,9 @@ public class StyledText implements TextFragment {
}
@Override
public float getWidth() throws IOException {
public float getWidth() {
if (width == null) {
width = getFontDescriptor().getSize()
* getFontDescriptor().getFont().getStringWidth(getText())
/ 1000;
width = getWidth(getFontDescriptor(), getText());
width += leftMargin;
width += rightMargin;
}
@ -149,7 +108,7 @@ public class StyledText implements TextFragment {
}
@Override
public float getHeight() throws IOException {
public float getHeight() {
return getFontDescriptor().getSize();
}
@ -158,9 +117,7 @@ public class StyledText implements TextFragment {
* @throws IOException by pdfbox.
*/
public float getAsent() throws IOException {
return getFontDescriptor().getSize()
* getFontDescriptor().getFont().getFontDescriptor().getAscent()
/ 1000;
return getFontDescriptor().getSize() * getFontDescriptor().getSelectedFont().getFontDescriptor().getAscent() / 1000;
}
public float getBaselineOffset() {
@ -206,12 +163,19 @@ public class StyledText implements TextFragment {
return inheritAttributes(text, getLeftMargin(), getRightMargin());
}
public StyledText inheritAttributes(String text, float leftMargin,
float rightMargin) {
public StyledText inheritAttributes(String text, float leftMargin, float rightMargin) {
return new StyledText(text, getFontDescriptor(), getColor(),
getBaselineOffset(), leftMargin, rightMargin);
}
private static float getWidth(FontDescriptor fontDescriptor, String text) {
try {
return fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getStringWidth(text) / 1000;
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public String toString() {
return "StyledText [text=" + text + ", fontDescriptor="
@ -219,5 +183,4 @@ public class StyledText implements TextFragment {
+ ", leftMargin=" + leftMargin + ", rightMargin=" + rightMargin
+ ", baselineOffset=" + baselineOffset + "]";
}
}

View file

@ -2,7 +2,7 @@ package org.xbib.graphics.pdfbox.layout.text;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.xbib.graphics.pdfbox.layout.font.Font;
import java.io.IOException;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
@ -68,9 +68,8 @@ public class TextFlow implements TextSequence, WidthRespecting {
return (T) cache.get(key);
}
public void addText(final String text, final float fontSize,
final Font font) throws IOException {
add(TextFlowUtil.createTextFlow(text, fontSize, font));
public void addText(String text, float fontSize, Font font) {
add(TextFlowUtil.createTextFlow(text, new FontDescriptor(font, fontSize)));
}
/**
@ -78,13 +77,20 @@ public class TextFlow implements TextSequence, WidthRespecting {
*
* @param markup the markup to add.
* @param fontSize the font size to use.
* @param baseFont the base font describing the bundle of
* plain/blold/italic/bold-italic fonts.
* @throws IOException by PDFBox
* @param font the font
*/
public void addMarkup(final String markup, final float fontSize,
final Font baseFont) throws IOException {
add(TextFlowUtil.createTextFlowFromMarkup(markup, fontSize, baseFont));
public void addMarkup(String markup, float fontSize, Font font) {
add(TextFlowUtil.createTextFlowFromMarkup(markup, new FontDescriptor(font, fontSize)));
}
public void addIndent(String label, float indentWidth, SpaceUnit indentUnit,
float fontsize, Font font) {
add(new Indent(label, indentWidth, indentUnit, new FontDescriptor(font, fontsize)));
}
public void addIndent(String label, float indentWidth, SpaceUnit indentUnit,
float fontsize, Font font, Alignment alignment) {
add(new Indent(label, indentWidth, indentUnit, new FontDescriptor(font, fontsize), alignment));
}
/**
@ -92,7 +98,7 @@ public class TextFlow implements TextSequence, WidthRespecting {
*
* @param sequence the sequence to add.
*/
public void add(final TextSequence sequence) {
public void add(TextSequence sequence) {
for (TextFragment fragment : sequence) {
add(fragment);
}
@ -103,7 +109,7 @@ public class TextFlow implements TextSequence, WidthRespecting {
*
* @param fragment the fragment to add.
*/
public void add(final TextFragment fragment) {
public void add(TextFragment fragment) {
text.add(fragment);
clearCache();
}
@ -199,7 +205,7 @@ public class TextFlow implements TextSequence, WidthRespecting {
}
@Override
public float getWidth() throws IOException {
public float getWidth() {
Float width = getCachedValue(WIDTH, Float.class);
if (width == null) {
width = TextSequenceUtil.getWidth(this, getMaxWidth());
@ -209,7 +215,7 @@ public class TextFlow implements TextSequence, WidthRespecting {
}
@Override
public float getHeight() throws IOException {
public float getHeight() {
Float height = getCachedValue(HEIGHT, Float.class);
if (height == null) {
height = TextSequenceUtil.getHeight(this, getMaxWidth(),
@ -221,23 +227,22 @@ public class TextFlow implements TextSequence, WidthRespecting {
@Override
public void drawText(PDPageContentStream contentStream, Position upperLeft,
Alignment alignment, DrawListener drawListener) throws IOException {
Alignment alignment, DrawListener drawListener) {
TextSequenceUtil.drawText(this, contentStream, upperLeft, drawListener, alignment,
getMaxWidth(), getLineSpacing(),
isApplyLineSpacingToFirstLine());
}
public void drawTextRightAligned(PDPageContentStream contentStream,
Position endOfFirstLine, DrawListener drawListener) throws IOException {
Position endOfFirstLine, DrawListener drawListener) {
drawText(contentStream, endOfFirstLine.add(-getWidth(), 0),
Alignment.RIGHT, drawListener);
}
/**
* @return a copy of this text flow where all leading {@link NewLine}s are removed.
* @throws IOException by pdfbox.
*/
public TextFlow removeLeadingEmptyLines() throws IOException {
public TextFlow removeLeadingEmptyLines() {
if (text.size() == 0 || !(text.get(0) instanceof NewLine)) {
return this;
}

View file

@ -1,12 +1,10 @@
package org.xbib.graphics.pdfbox.layout.text;
import org.apache.pdfbox.pdmodel.font.PDFont;
import org.xbib.graphics.pdfbox.layout.font.Font;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotatedStyledText;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotation;
import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationCharacters;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -17,11 +15,9 @@ import java.util.regex.Matcher;
public class TextFlowUtil {
public static TextFlow createTextFlow(String text,
float fontSize,
Font baseFont) throws IOException {
public static TextFlow createTextFlow(String text, FontDescriptor descriptor) {
final Iterable<CharSequence> parts = fromPlainText(text);
return createTextFlow(parts, fontSize, baseFont);
return createTextFlow(parts, descriptor);
}
/**
@ -52,41 +48,33 @@ public class TextFlowUtil {
* </pre>
*
* @param markup the markup text.
* @param fontSize the font size to use.
* @param baseFont the font.
* @param descriptor the font size to use, and the font.
* @return the created text flow.
* @throws IOException by pdfbox
*/
public static TextFlow createTextFlowFromMarkup(final String markup,
final float fontSize,
Font baseFont) throws IOException {
public static TextFlow createTextFlowFromMarkup(String markup, FontDescriptor descriptor) {
final Iterable<CharSequence> parts = fromMarkup(markup);
return createTextFlow(parts, fontSize, baseFont);
return createTextFlow(parts, descriptor);
}
/**
* Actually creates the text flow from the given (markup) text.
*
* @param parts the parts to create the text flow from.
* @param fontSize the font size to use.
* @param descriptor the font size to use.
* @return the created text flow.
* @throws IOException by pdfbox
*/
protected static TextFlow createTextFlow(final Iterable<CharSequence> parts,
final float fontSize, Font baseFont)
throws IOException {
protected static TextFlow createTextFlow(Iterable<CharSequence> parts, FontDescriptor descriptor) {
final TextFlow result = new TextFlow();
boolean bold = false;
boolean italic = false;
Color color = Color.black;
ControlCharacters.MetricsControlCharacter metricsControl = null;
Map<Class<? extends Annotation>, Annotation> annotationMap = new HashMap<Class<? extends Annotation>, Annotation>();
Stack<IndentCharacters.IndentCharacter> indentStack = new Stack<IndentCharacters.IndentCharacter>();
Map<Class<? extends Annotation>, Annotation> annotationMap = new HashMap<>();
Stack<IndentCharacters.IndentCharacter> indentStack = new Stack<>();
for (final CharSequence fragment : parts) {
if (fragment instanceof ControlCharacter) {
if (fragment instanceof ControlCharacters.NewLineControlCharacter) {
result.add(new NewLine(fontSize));
result.add(new NewLine(descriptor));
}
if (fragment instanceof ControlCharacters.BoldControlCharacter) {
bold = !bold;
@ -110,7 +98,6 @@ public class TextFlowUtil {
}
if (fragment instanceof ControlCharacters.MetricsControlCharacter) {
if (metricsControl != null && metricsControl.toString().equals(fragment.toString())) {
// end marker
metricsControl = null;
} else {
metricsControl = (ControlCharacters.MetricsControlCharacter) fragment;
@ -119,10 +106,8 @@ public class TextFlowUtil {
if (fragment instanceof IndentCharacters.IndentCharacter) {
IndentCharacters.IndentCharacter currentIndent = (IndentCharacters.IndentCharacter) fragment;
if (currentIndent.getLevel() == 0) {
// indentation of 0 resets indent
indentStack.clear();
result.add(Indent.UNINDENT);
continue;
} else {
IndentCharacters.IndentCharacter last = null;
while (!indentStack.isEmpty()
@ -135,26 +120,27 @@ public class TextFlowUtil {
currentIndent = last;
}
indentStack.push(currentIndent);
result.add(currentIndent.createNewIndent(fontSize,
baseFont.getPlainFont(), color));
FontDescriptor fontDescriptor =
new FontDescriptor(descriptor.getFont(), descriptor.getSize(), bold, italic);
result.add(currentIndent.createNewIndent(fontDescriptor, color));
}
}
} else {
PDFont font = getFont(bold, italic, baseFont);
float baselineOffset = 0;
float currentFontSize = fontSize;
float currentFontSize = descriptor.getSize();
if (metricsControl != null) {
baselineOffset = metricsControl.getBaselineOffsetScale() * fontSize;
baselineOffset = metricsControl.getBaselineOffsetScale() * descriptor.getSize();
currentFontSize *= metricsControl.getFontScale();
}
FontDescriptor fontDescriptor = new FontDescriptor(descriptor.getFont(), currentFontSize, bold, italic);
if (annotationMap.isEmpty()) {
StyledText styledText = new StyledText(fragment.toString(),
currentFontSize, font, color, baselineOffset);
StyledText styledText = new StyledText(fragment.toString(), fontDescriptor,
color, baselineOffset, 0, 0);
result.add(styledText);
} else {
AnnotatedStyledText styledText = new AnnotatedStyledText(
fragment.toString(), currentFontSize, baseFont, color, baselineOffset,
annotationMap.values());
AnnotatedStyledText styledText =
new AnnotatedStyledText(fragment.toString(), fontDescriptor,
color, baselineOffset, 0, 0, annotationMap.values());
result.add(styledText);
}
}
@ -162,33 +148,6 @@ public class TextFlowUtil {
return result;
}
protected static PDFont getFont(boolean bold, boolean italic,
final PDFont plainFont, final PDFont boldFont,
final PDFont italicFont, final PDFont boldItalicFont) {
PDFont font = plainFont;
if (bold && !italic) {
font = boldFont;
} else if (!bold && italic) {
font = italicFont;
} else if (bold) {
font = boldItalicFont;
}
return font;
}
protected static PDFont getFont(boolean bold, boolean italic,
Font baseFont) {
PDFont font = baseFont.getPlainFont();
if (bold && !italic) {
font = baseFont.getBoldFont();
} else if (!bold && italic) {
font = baseFont.getItalicFont();
} else if (bold) {
font = baseFont.getBoldItalicFont();
}
return font;
}
/**
* Creates a char sequence where new-line is replaced by the corresponding
* {@link ControlCharacter}.
@ -233,25 +192,19 @@ public class TextFlowUtil {
* @param markup the markup.
* @return the create char sequence.
*/
public static Iterable<CharSequence> fromMarkup(
final Iterable<CharSequence> markup) {
public static Iterable<CharSequence> fromMarkup(Iterable<CharSequence> markup) {
Iterable<CharSequence> text = markup;
text = splitByControlCharacter(ControlCharacters.NEWLINE_FACTORY, text);
text = splitByControlCharacter(ControlCharacters.METRICS_FACTORY, text);
text = splitByControlCharacter(ControlCharacters.BOLD_FACTORY, text);
text = splitByControlCharacter(ControlCharacters.ITALIC_FACTORY, text);
text = splitByControlCharacter(ControlCharacters.COLOR_FACTORY, text);
for (AnnotationCharacters.AnnotationControlCharacterFactory<?> annotationControlCharacterFactory : AnnotationCharacters
.getFactories()) {
text = splitByControlCharacter(annotationControlCharacterFactory,
text);
text = splitByControlCharacter(annotationControlCharacterFactory, text);
}
text = splitByControlCharacter(IndentCharacters.INDENT_FACTORY, text);
text = unescapeBackslash(text);
return text;
}
@ -266,13 +219,12 @@ public class TextFlowUtil {
protected static Iterable<CharSequence> splitByControlCharacter(
ControlCharacters.ControlCharacterFactory controlCharacterFactory,
final Iterable<CharSequence> markup) {
List<CharSequence> result = new ArrayList<CharSequence>();
List<CharSequence> result = new ArrayList<>();
boolean beginOfLine = true;
for (CharSequence current : markup) {
if (current instanceof String) {
String string = (String) current;
int begin = 0;
if (!controlCharacterFactory.patternMatchesBeginOfLine()
|| beginOfLine) {
Matcher matcher = controlCharacterFactory.getPattern()
@ -280,24 +232,18 @@ public class TextFlowUtil {
while (matcher.find()) {
String part = string.substring(begin, matcher.start());
begin = matcher.end();
if (!part.isEmpty()) {
String unescaped = controlCharacterFactory
.unescape(part);
String unescaped = controlCharacterFactory.unescape(part);
result.add(unescaped);
}
result.add(controlCharacterFactory
.createControlCharacter(string, matcher, result));
result.add(controlCharacterFactory.createControlCharacter(string, matcher, result));
}
}
if (begin < string.length()) {
String part = string.substring(begin);
String unescaped = controlCharacterFactory.unescape(part);
result.add(unescaped);
}
beginOfLine = false;
} else {
if (current instanceof ControlCharacters.NewLineControlCharacter) {
@ -305,23 +251,19 @@ public class TextFlowUtil {
}
result.add(current);
}
}
return result;
}
private static Iterable<CharSequence> unescapeBackslash(
final Iterable<CharSequence> chars) {
List<CharSequence> result = new ArrayList<CharSequence>();
private static Iterable<CharSequence> unescapeBackslash(Iterable<CharSequence> chars) {
List<CharSequence> result = new ArrayList<>();
for (CharSequence current : chars) {
if (current instanceof String) {
result.add(ControlCharacters
.unescapeBackslash((String) current));
result.add(ControlCharacters.unescapeBackslash((String) current));
} else {
result.add(current);
}
}
return result;
}
}

View file

@ -5,6 +5,7 @@ import org.apache.pdfbox.util.Matrix;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import java.awt.Color;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -109,7 +110,7 @@ public class TextLine implements TextSequence {
}
@Override
public float getWidth() throws IOException {
public float getWidth() {
Float width = getCachedValue(WIDTH, Float.class);
if (width == null) {
width = 0f;
@ -122,7 +123,7 @@ public class TextLine implements TextSequence {
}
@Override
public float getHeight() throws IOException {
public float getHeight() {
Float height = getCachedValue(HEIGHT, Float.class);
if (height == null) {
height = 0f;
@ -136,16 +137,14 @@ public class TextLine implements TextSequence {
/**
* @return the (max) ascent of this line.
* @throws IOException by pdfbox.
*/
protected float getAscent() throws IOException {
protected float getAscent() {
Float ascent = getCachedValue(ASCENT, Float.class);
if (ascent == null) {
ascent = 0f;
for (TextFragment fragment : this) {
float currentAscent = fragment.getFontDescriptor().getSize()
* fragment.getFontDescriptor().getFont()
.getFontDescriptor().getAscent() / 1000;
FontDescriptor fontDescriptor = fragment.getFontDescriptor();
float currentAscent = fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getFontDescriptor().getAscent() / 1000;
ascent = Math.max(ascent, currentAscent);
}
setCachedValue(ASCENT, ascent);
@ -155,13 +154,16 @@ public class TextLine implements TextSequence {
@Override
public void drawText(PDPageContentStream contentStream, Position upperLeft,
Alignment alignment, DrawListener drawListener) throws IOException {
Alignment alignment, DrawListener drawListener) {
drawAligned(contentStream, upperLeft, alignment, getWidth(), drawListener);
}
public void drawAligned(PDPageContentStream contentStream, Position upperLeft,
Alignment alignment, float availableLineWidth,
DrawListener drawListener) throws IOException {
public void drawAligned(PDPageContentStream contentStream,
Position upperLeft,
Alignment alignment,
float availableLineWidth,
DrawListener drawListener) {
try {
contentStream.saveGraphicsState();
float x = upperLeft.getX();
float y = upperLeft.getY() - getAscent();
@ -191,7 +193,7 @@ public class TextLine implements TextSequence {
contentStream.setTextMatrix(matrix);
if (!styledText.getFontDescriptor().equals(lastFontDesc)) {
lastFontDesc = styledText.getFontDescriptor();
contentStream.setFont(lastFontDesc.getFont(), lastFontDesc.getSize());
contentStream.setFont(lastFontDesc.getSelectedFont(), lastFontDesc.getSize());
}
if (!styledText.getColor().equals(lastColor)) {
lastColor = styledText.getColor();
@ -202,9 +204,8 @@ public class TextLine implements TextSequence {
}
contentStream.endText();
if (drawListener != null) {
float currentUpperLeft = y + styledText.getAsent();
drawListener.drawn(styledText,
new Position(x, currentUpperLeft),
new Position(x, y + styledText.getAsent()),
styledText.getWidthWithoutMargin(),
styledText.getHeight());
}
@ -215,6 +216,9 @@ public class TextLine implements TextSequence {
}
}
contentStream.restoreGraphicsState();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
@Override

View file

@ -7,6 +7,7 @@ import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import org.xbib.graphics.pdfbox.layout.util.Pair;
import org.xbib.graphics.pdfbox.layout.util.WordBreakerFactory;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
@ -20,21 +21,16 @@ public class TextSequenceUtil {
*
* @param text the text to extract the lines from.
* @return the list of text lines.
* @throws IOException by pdfbox
*/
public static List<TextLine> getLines(final TextSequence text)
throws IOException {
final List<TextLine> result = new ArrayList<TextLine>();
public static List<TextLine> getLines(final TextSequence text) {
final List<TextLine> result = new ArrayList<>();
TextLine line = new TextLine();
for (TextFragment fragment : text) {
if (fragment instanceof NewLine) {
line.setNewLine((NewLine) fragment);
result.add(line);
line = new TextLine();
} else if (fragment instanceof ReplacedWhitespace) {
// ignore replaced whitespace
} else {
} else if (!(fragment instanceof ReplacedWhitespace)) {
line.add((StyledText) fragment);
}
}
@ -51,13 +47,11 @@ public class TextSequenceUtil {
* @param maxWidth the max width used for word-wrapping.
* @param maxHeight the max height for divide.
* @return the Divided element containing the parts.
* @throws IOException by pdfbox
*/
public static Divided divide(final TextSequence text, final float maxWidth,
final float maxHeight) throws IOException {
final float maxHeight) {
TextFlow wrapped = wordWrap(text, maxWidth);
List<TextLine> lines = getLines(wrapped);
Paragraph first = new Paragraph();
Paragraph tail = new Paragraph();
if (text instanceof TextFlow) {
@ -74,24 +68,19 @@ public class TextSequenceUtil {
tail.setAlignment(paragraph.getAlignment());
tail.setApplyLineSpacingToFirstLine(paragraph.isApplyLineSpacingToFirstLine());
}
int index = 0;
do {
TextLine line = lines.get(index);
first.add(line);
++index;
} while (index < lines.size() && first.getHeight() < maxHeight);
if (first.getHeight() > maxHeight) {
// remove last line
--index;
TextLine line = lines.get(index);
for (@SuppressWarnings("unused")
TextFragment textFragment : line) {
for (TextFragment textFragment : line) {
first.removeLast();
}
}
for (int i = index; i < lines.size(); ++i) {
tail.add(lines.get(i));
}
@ -104,11 +93,8 @@ public class TextSequenceUtil {
* @param text the text to word-wrap.
* @param maxWidth the max width to fit.
* @return the word-wrapped text.
* @throws IOException by pdfbox
*/
public static TextFlow wordWrap(final TextSequence text,
final float maxWidth) throws IOException {
public static TextFlow wordWrap(TextSequence text, float maxWidth) {
float indentation = 0;
TextFlow result = new TextFlow();
float lineLength = indentation;
@ -149,7 +135,7 @@ public class TextSequenceUtil {
}
private static WordWrapContext wordWrap(final WordWrapContext context,
final float maxWidth, final TextFlow result) throws IOException {
final float maxWidth, final TextFlow result) {
TextFragment word = context.getWord();
TextFragment moreToWrap = null;
float indentation = context.getIndentation();
@ -270,9 +256,8 @@ public class TextSequenceUtil {
*
* @param text the text to de-wrap.
* @return the de-wrapped text.
* @throws IOException by PDFBox
*/
public static TextFlow deWrap(final TextSequence text) throws IOException {
public static TextFlow deWrap(final TextSequence text) {
TextFlow result = new TextFlow();
for (TextFragment fragment : text) {
if (fragment instanceof WrappingNewLine) {
@ -297,13 +282,10 @@ public class TextSequenceUtil {
* @param text the text to word-wrap.
* @param maxWidth the max width to fit.
* @return the word-wrapped text lines.
* @throws IOException by pdfbox
*/
public static List<TextLine> wordWrapToLines(final TextSequence text,
final float maxWidth) throws IOException {
TextFlow wrapped = wordWrap(text, maxWidth);
List<TextLine> lines = getLines(wrapped);
return lines;
final float maxWidth) {
return getLines(wordWrap(text, maxWidth));
}
/**
@ -367,7 +349,7 @@ public class TextSequenceUtil {
private static Pair<TextFragment> breakWord(TextFragment word,
float wordWidth, final float remainingLineWidth, float maxWidth,
boolean breakHard) throws IOException {
boolean breakHard) {
float leftMargin = 0;
float rightMargin = 0;
@ -377,8 +359,7 @@ public class TextSequenceUtil {
rightMargin = styledText.getRightMargin();
}
Pair<String> brokenWord = WordBreakerFactory.getWorkBreaker()
.breakWord(word.getText(), word.getFontDescriptor(),
Pair<String> brokenWord = WordBreakerFactory.getWorkBreaker().breakWord(word.getText(), word.getFontDescriptor(),
remainingLineWidth - leftMargin, breakHard);
if (brokenWord == null) {
return null;
@ -398,10 +379,8 @@ public class TextSequenceUtil {
*
* @param fontDescriptor font and size.
* @return the width of <code>M</code>.
* @throws IOException by pdfbox
*/
public static float getEmWidth(final FontDescriptor fontDescriptor)
throws IOException {
public static float getEmWidth(final FontDescriptor fontDescriptor) {
return getStringWidth("M", fontDescriptor);
}
@ -411,12 +390,13 @@ public class TextSequenceUtil {
* @param text the text to measure.
* @param fontDescriptor font and size.
* @return the width of given text.
* @throws IOException by pdfbox
*/
public static float getStringWidth(final String text,
final FontDescriptor fontDescriptor) throws IOException {
return fontDescriptor.getSize()
* fontDescriptor.getFont().getStringWidth(text) / 1000;
public static float getStringWidth(String text, FontDescriptor fontDescriptor) {
try {
return fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getStringWidth(text) / 1000;
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
/**
@ -434,13 +414,13 @@ public class TextSequenceUtil {
* @param lineSpacing the line spacing factor.
* @param applyLineSpacingToFirstLine indicates if the line spacing should be applied to the first
* line also. Makes sense in most cases to do so.
* @throws IOException by pdfbox
*/
public static void drawText(TextSequence text,
PDPageContentStream contentStream, Position upperLeft,
DrawListener drawListener, Alignment alignment, float maxWidth,
final float lineSpacing, final boolean applyLineSpacingToFirstLine)
throws IOException {
PDPageContentStream contentStream,
Position upperLeft,
DrawListener drawListener,
Alignment alignment,
float maxWidth, final float lineSpacing, final boolean applyLineSpacingToFirstLine) {
List<TextLine> lines = wordWrapToLines(text, maxWidth);
float maxLineWidth = Math.max(maxWidth, getMaxWidth(lines));
Position position = upperLeft;
@ -468,11 +448,9 @@ public class TextSequenceUtil {
* @param targetWidth the target width
* @param alignment the alignment of the line.
* @return the left offset.
* @throws IOException by pdfbox
*/
public static float getOffset(final TextSequence textLine,
final float targetWidth, final Alignment alignment)
throws IOException {
final float targetWidth, final Alignment alignment) {
switch (alignment) {
case RIGHT:
return targetWidth - textLine.getWidth();
@ -488,10 +466,8 @@ public class TextSequenceUtil {
*
* @param lines the lines for which to calculate the max width.
* @return the max width of the lines.
* @throws IOException by pdfbox.
*/
public static float getMaxWidth(final Iterable<TextLine> lines)
throws IOException {
public static float getMaxWidth(Iterable<TextLine> lines) {
float max = 0;
for (TextLine line : lines) {
max = Math.max(max, line.getWidth());
@ -505,10 +481,8 @@ public class TextSequenceUtil {
* @param textSequence the text.
* @param maxWidth if &gt; 0, the text may be word-wrapped to match the width.
* @return the width of the text.
* @throws IOException by pdfbox.
*/
public static float getWidth(final TextSequence textSequence,
final float maxWidth) throws IOException {
public static float getWidth(TextSequence textSequence, float maxWidth) {
List<TextLine> lines = wordWrapToLines(textSequence, maxWidth);
float max = 0;
for (TextLine line : lines) {
@ -526,11 +500,10 @@ public class TextSequenceUtil {
* @param applyLineSpacingToFirstLine indicates if the line spacing should be applied to the first
* line also. Makes sense in most cases to do so.
* @return the height of the text.
* @throws IOException by pdfbox
*/
public static float getHeight(final TextSequence textSequence,
final float maxWidth, final float lineSpacing,
final boolean applyLineSpacingToFirstLine) throws IOException {
final boolean applyLineSpacingToFirstLine) {
List<TextLine> lines = wordWrapToLines(textSequence, maxWidth);
float sum = 0;
for (int i = 0; i < lines.size(); i++) {
@ -546,9 +519,13 @@ public class TextSequenceUtil {
}
private static class WordWrapContext {
private final TextFragment word;
private final float lineLength;
private final float indentation;
boolean isWrappedLine;
public WordWrapContext(TextFragment word, float lineLength,
@ -578,6 +555,5 @@ public class TextSequenceUtil {
public boolean isMoreToWrap() {
return getWord() != null;
}
}
}

View file

@ -1,6 +1,5 @@
package org.xbib.graphics.pdfbox.layout.text;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
/**
@ -9,13 +8,6 @@ import org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
*/
public class WrappingNewLine extends NewLine {
/**
* See {@link NewLine#NewLine()}.
*/
public WrappingNewLine() {
super();
}
/**
* See {@link NewLine#NewLine(FontDescriptor)}.
*
@ -24,15 +16,4 @@ public class WrappingNewLine extends NewLine {
public WrappingNewLine(FontDescriptor fontDescriptor) {
super(fontDescriptor);
}
/**
* See {@link NewLine#NewLine(float)}.
*
* @param fontSize the font size, resp. the height of the new line.
*/
public WrappingNewLine(float fontSize) {
super(fontSize);
}
}

View file

@ -17,23 +17,6 @@ public class AnnotatedStyledText extends StyledText implements Annotated {
private final List<Annotation> annotations = new ArrayList<>();
/**
* Creates a styled text.
*
* @param text the text to draw. Must not contain line feeds ('\n').
* @param size the size of the font.
* @param font the font to use..
* @param color the color to use.
* @param baselineOffset the offset of the baseline.
* @param annotations the annotations associated with the text.
*/
public AnnotatedStyledText(String text, float size, Font font,
Color color, final float baselineOffset,
Collection<? extends Annotation> annotations) {
this(text, new FontDescriptor(font.getPlainFont(), size), color, baselineOffset, 0, 0, annotations);
}
/**
* Creates a styled text.
*
@ -52,8 +35,7 @@ public class AnnotatedStyledText extends StyledText implements Annotated {
final float rightMargin,
final float baselineOffset,
Collection<? extends Annotation> annotations) {
super(text, fontDescriptor, color, baselineOffset, leftMargin,
rightMargin);
super(text, fontDescriptor, color, baselineOffset, leftMargin, rightMargin);
if (annotations != null) {
this.annotations.addAll(annotations);
}

View file

@ -6,8 +6,8 @@ import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.AnchorAnnota
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation.LinkStyle;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.UnderlineAnnotation;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -16,7 +16,7 @@ import java.util.regex.Pattern;
*/
public class AnnotationCharacters {
private final static List<AnnotationControlCharacterFactory<?>> FACTORIES = new CopyOnWriteArrayList<>();
private final static List<AnnotationControlCharacterFactory<?>> FACTORIES = new ArrayList<>();
static {
register(new HyperlinkControlCharacterFactory());
@ -30,8 +30,7 @@ public class AnnotationCharacters {
*
* @param factory the factory to register.
*/
public static void register(
final AnnotationControlCharacterFactory<?> factory) {
public static void register(AnnotationControlCharacterFactory<?> factory) {
FACTORIES.add(factory);
}
@ -97,8 +96,7 @@ public class AnnotationCharacters {
@Override
public String unescape(String text) {
return text
.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
return text.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
}
@Override
@ -119,8 +117,7 @@ public class AnnotationCharacters {
@Override
public UnderlineControlCharacter createControlCharacter(String text,
Matcher matcher, final List<CharSequence> charactersSoFar) {
return new UnderlineControlCharacter(matcher.group(4),
matcher.group(6));
return new UnderlineControlCharacter(matcher.group(4), matcher.group(6));
}
@Override
@ -130,8 +127,7 @@ public class AnnotationCharacters {
@Override
public String unescape(String text) {
return text
.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
return text.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
}
@Override
@ -149,6 +145,7 @@ public class AnnotationCharacters {
*/
public static class HyperlinkControlCharacter extends
AnnotationControlCharacter<HyperlinkAnnotation> {
private HyperlinkAnnotation hyperlink;
protected HyperlinkControlCharacter(final String hyperlink,

View file

@ -9,7 +9,6 @@ import org.xbib.graphics.pdfbox.layout.text.DrawContext;
import org.xbib.graphics.pdfbox.layout.text.DrawListener;
import org.xbib.graphics.pdfbox.layout.text.DrawableText;
import org.xbib.graphics.pdfbox.layout.text.Position;
import java.io.IOException;
/**
* This listener has to be passed to all
@ -44,59 +43,35 @@ public class AnnotationDrawListener implements DrawListener, RenderListener {
return;
}
for (AnnotationProcessor annotationProcessor : annotationProcessors) {
try {
annotationProcessor.annotatedObjectDrawn(
(Annotated) drawnObject, drawContext, upperLeft, width,
height);
} catch (IOException e) {
throw new RuntimeException(
"exception on annotation processing", e);
}
}
}
/**
* @throws IOException by pdfbox.
* @deprecated user {@link #afterRender()} instead.
*/
@Deprecated
public void finalizeAnnotations() throws IOException {
public void finalizeAnnotations() {
afterRender();
}
@Override
public void beforePage(RenderContext renderContext) throws IOException {
public void beforePage(RenderContext renderContext) {
for (AnnotationProcessor annotationProcessor : annotationProcessors) {
try {
annotationProcessor.beforePage(drawContext);
} catch (IOException e) {
throw new RuntimeException(
"exception on annotation processing", e);
}
}
}
@Override
public void afterPage(RenderContext renderContext) throws IOException {
public void afterPage(RenderContext renderContext) {
for (AnnotationProcessor annotationProcessor : annotationProcessors) {
try {
annotationProcessor.afterPage(drawContext);
} catch (IOException e) {
throw new RuntimeException(
"exception on annotation processing", e);
}
}
}
public void afterRender() throws IOException {
public void afterRender() {
for (AnnotationProcessor annotationProcessor : annotationProcessors) {
try {
annotationProcessor.afterRender(drawContext.getPdDocument());
} catch (IOException e) {
throw new RuntimeException(
"exception on annotation processing", e);
}
}
}

View file

@ -3,7 +3,6 @@ package org.xbib.graphics.pdfbox.layout.text.annotations;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.xbib.graphics.pdfbox.layout.text.DrawContext;
import org.xbib.graphics.pdfbox.layout.text.Position;
import java.io.IOException;
/**
* Processes an annotation.
@ -18,34 +17,30 @@ public interface AnnotationProcessor {
* @param upperLeft the upper left position the object has been drawn to.
* @param width the width of the drawn object.
* @param height the height of the drawn object.
* @throws IOException by pdfbox.
*/
void annotatedObjectDrawn(final Annotated drawnObject,
final DrawContext drawContext, Position upperLeft, float width,
float height) throws IOException;
float height);
/**
* Called before a page is drawn.
*
* @param drawContext the drawing context.
* @throws IOException by pdfbox.
*/
void beforePage(final DrawContext drawContext) throws IOException;
void beforePage(final DrawContext drawContext);
/**
* Called after a page is drawn.
*
* @param drawContext the drawing context.
* @throws IOException by pdfbox.
*/
void afterPage(final DrawContext drawContext) throws IOException;
void afterPage(final DrawContext drawContext);
/**
* Called after all rendering has been performed.
*
* @param document the document.
* @throws IOException by pdfbox.
*/
void afterRender(final PDDocument document) throws IOException;
void afterRender(final PDDocument document);
}

View file

@ -3,16 +3,23 @@ package org.xbib.graphics.pdfbox.layout.text.annotations;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
import org.xbib.graphics.pdfbox.layout.text.DrawContext;
import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.AnchorAnnotation;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation.LinkStyle;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -26,13 +33,13 @@ import java.util.Map.Entry;
*/
public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
private final Map<String, PageAnchor> anchorMap = new HashMap<String, PageAnchor>();
private final Map<PDPage, List<Hyperlink>> linkMap = new HashMap<PDPage, List<Hyperlink>>();
private final Map<String, PageAnchor> anchorMap = new HashMap<>();
private final Map<PDPage, List<Hyperlink>> linkMap = new HashMap<>();
@Override
public void annotatedObjectDrawn(Annotated drawnObject,
DrawContext drawContext, Position upperLeft, float width,
float height) throws IOException {
float height) {
if (!(drawnObject instanceof AnnotatedStyledText)) {
return;
@ -85,26 +92,107 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
}
@Override
public void afterRender(PDDocument document) throws IOException {
public void afterRender(PDDocument document) {
for (Entry<PDPage, List<Hyperlink>> entry : linkMap.entrySet()) {
PDPage page = entry.getKey();
List<Hyperlink> links = entry.getValue();
for (Hyperlink hyperlink : links) {
PDAnnotationLink pdLink = null;
PDAnnotationLink pdLink;
if (hyperlink.getHyperlinkURI().startsWith("#")) {
pdLink = createGotoLink(hyperlink);
} else {
pdLink = CompatibilityHelper.createLink(page,
pdLink = createLink(page,
hyperlink.getRect(), hyperlink.getColor(),
hyperlink.getLinkStyle(),
hyperlink.getHyperlinkURI());
}
try {
page.getAnnotations().add(pdLink);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
}
private static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color,
LinkStyle linkStyle, final String uri) {
PDAnnotationLink pdLink = createLink(page, rect, color, linkStyle);
PDActionURI actionUri = new PDActionURI();
actionUri.setURI(uri);
pdLink.setAction(actionUri);
return pdLink;
}
private static PDBorderStyleDictionary noBorder;
private static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color,
LinkStyle linkStyle) {
PDAnnotationLink pdLink = new PDAnnotationLink();
if (linkStyle == LinkStyle.none) {
if (noBorder == null) {
noBorder = new PDBorderStyleDictionary();
noBorder.setWidth(0);
}
return pdLink;
}
PDBorderStyleDictionary borderStyle = new PDBorderStyleDictionary();
borderStyle.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE);
pdLink.setBorderStyle(borderStyle);
PDRectangle rotatedRect = transformToPageRotation(rect, page);
pdLink.setRectangle(rotatedRect);
pdLink.setColor(toPDColor(color));
return pdLink;
}
private static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color,
LinkStyle linkStyle, final PDDestination destination) {
PDAnnotationLink pdLink = createLink(page, rect, color, linkStyle);
PDActionGoTo gotoAction = new PDActionGoTo();
gotoAction.setDestination(destination);
pdLink.setAction(gotoAction);
return pdLink;
}
private static PDRectangle transformToPageRotation(final PDRectangle rect, final PDPage page) {
AffineTransform transform = transformToPageRotation(page);
if (transform == null) {
return rect;
}
float[] points = new float[]{rect.getLowerLeftX(), rect.getLowerLeftY(), rect.getUpperRightX(), rect.getUpperRightY()};
float[] rotatedPoints = new float[4];
transform.transform(points, 0, rotatedPoints, 0, 2);
PDRectangle rotated = new PDRectangle();
rotated.setLowerLeftX(rotatedPoints[0]);
rotated.setLowerLeftY(rotatedPoints[1]);
rotated.setUpperRightX(rotatedPoints[2]);
rotated.setUpperRightY(rotatedPoints[3]);
return rotated;
}
private static AffineTransform transformToPageRotation(final PDPage page) {
int pageRotation = page.getRotation();
if (pageRotation == 0) {
return null;
}
float pageWidth = page.getMediaBox().getHeight();
float pageHeight = page.getMediaBox().getWidth();
AffineTransform transform = new AffineTransform();
transform.rotate(pageRotation * Math.PI / 180, pageHeight / 2,
pageWidth / 2);
double offset = Math.abs(pageHeight - pageWidth) / 2;
transform.translate(-offset, offset);
return transform;
}
private static PDColor toPDColor(final Color color) {
float[] components = {
color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f
};
return new PDColor(components, PDDeviceRGB.INSTANCE);
}
private PDAnnotationLink createGotoLink(Hyperlink hyperlink) {
String anchor = hyperlink.getHyperlinkURI().substring(1);
PageAnchor pageAnchor = anchorMap.get(anchor);
@ -116,7 +204,7 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
xyzDestination.setPage(pageAnchor.getPage());
xyzDestination.setLeft((int) pageAnchor.getX());
xyzDestination.setTop((int) pageAnchor.getY());
return CompatibilityHelper.createLink(pageAnchor.getPage(), hyperlink.getRect(),
return createLink(pageAnchor.getPage(), hyperlink.getRect(),
hyperlink.getColor(), hyperlink.getLinkStyle(), xyzDestination);
}
@ -147,7 +235,6 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
public String toString() {
return "PageAnchor [page=" + page + ", x=" + x + ", y=" + y + "]";
}
}
private static class Hyperlink {
@ -186,7 +273,5 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
+ ", hyperlinkUri=" + hyperlinkUri + ", linkStyle="
+ linkStyle + "]";
}
}
}

View file

@ -9,11 +9,12 @@ import org.xbib.graphics.pdfbox.layout.text.StyledText;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.UnderlineAnnotation;
import java.awt.Color;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
/**
* This annotation processor handles the {@link UnderlineAnnotation}s, and adds
* This annotation processor handles the {@link UnderlineAnnotation} and adds
* the needed hyperlink metadata to the PDF document.
*/
public class UnderlineAnnotationProcessor implements AnnotationProcessor {
@ -22,23 +23,20 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
@Override
public void annotatedObjectDrawn(Annotated drawnObject,
DrawContext drawContext, Position upperLeft, float width,
DrawContext drawContext,
Position upperLeft,
float width,
float height) {
if (!(drawnObject instanceof StyledText)) {
return;
}
StyledText drawnText = (StyledText) drawnObject;
for (UnderlineAnnotation underlineAnnotation : drawnObject
.getAnnotationsOfType(UnderlineAnnotation.class)) {
for (UnderlineAnnotation underlineAnnotation : drawnObject.getAnnotationsOfType(UnderlineAnnotation.class)) {
float fontSize = drawnText.getFontDescriptor().getSize();
float ascent = fontSize
* drawnText.getFontDescriptor().getFont()
.getFontDescriptor().getAscent() / 1000;
float ascent = fontSize * drawnText.getFontDescriptor().getSelectedFont().getFontDescriptor().getAscent() / 1000;
float baselineOffset = fontSize * underlineAnnotation.getBaselineOffsetScale();
float thickness = (0.01f + fontSize * 0.05f)
* underlineAnnotation.getLineWeight();
Position start = new Position(upperLeft.getX(), upperLeft.getY()
- ascent + baselineOffset);
float thickness = (0.01f + fontSize * 0.05f) * underlineAnnotation.getLineWeight();
Position start = new Position(upperLeft.getX(), upperLeft.getY() - ascent + baselineOffset);
Position end = new Position(start.getX() + width, start.getY());
Stroke stroke = Stroke.builder().lineWidth(thickness).build();
Line line = new Line(start, end, stroke, drawnText.getColor());
@ -47,12 +45,12 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
}
@Override
public void beforePage(DrawContext drawContext) throws IOException {
public void beforePage(DrawContext drawContext) {
linesOnPage.clear();
}
@Override
public void afterPage(DrawContext drawContext) throws IOException {
public void afterPage(DrawContext drawContext) {
for (Line line : linesOnPage) {
line.draw(drawContext.getCurrentPageContentStream());
}
@ -60,15 +58,18 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
}
@Override
public void afterRender(PDDocument document) throws IOException {
public void afterRender(PDDocument document) {
linesOnPage.clear();
}
private static class Line {
private final Position start;
private final Position end;
private final Stroke stroke;
private final Color color;
public Line(Position start, Position end, Stroke stroke, Color color) {
@ -79,7 +80,8 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
this.color = color;
}
public void draw(PDPageContentStream contentStream) throws IOException {
public void draw(PDPageContentStream contentStream) {
try {
if (color != null) {
contentStream.setStrokingColor(color);
}
@ -88,6 +90,9 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
}
contentStream.moveTo(start.getX(), start.getY());
contentStream.lineTo(end.getX(), end.getY());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
}

View file

@ -1,229 +0,0 @@
package org.xbib.graphics.pdfbox.layout.util;
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.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo;
import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotation;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink;
import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary;
import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination;
import org.apache.pdfbox.util.Matrix;
import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation.LinkStyle;
import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
public class CompatibilityHelper {
private static final String BULLET = "\u2022";
private static final String DOUBLE_ANGLE = "\u00bb";
private static PDBorderStyleDictionary noBorder;
/**
* Returns the bullet character for the given level. Actually only two
* bullets are used for odd and even levels. For odd levels the
* {@link #BULLET bullet} character is used, for even it is the
* {@link #DOUBLE_ANGLE double angle}. You may customize this by setting the
* system properties <code>pdfbox.layout.bullet.odd</code> and/or
* <code>pdfbox.layout.bullet.even</code>.
*
* @param level the level to return the bullet for.
* @return the bullet character for the leve.
*/
public static String getBulletCharacter(final int level) {
if (level % 2 == 1) {
return System.getProperty("pdfbox.layout.bullet.odd", BULLET);
}
return System.getProperty("pdfbox.layout.bullet.even", DOUBLE_ANGLE);
}
public static void clip(final PDPageContentStream contentStream)
throws IOException {
contentStream.clip();
}
public static void transform(final PDPageContentStream contentStream,
float a, float b, float c, float d, float e, float f)
throws IOException {
contentStream.transform(new Matrix(a, b, c, d, e, f));
}
public static void curveTo(final PDPageContentStream contentStream, float x1, float y1, float x2, float y2, float x3, float y3) throws IOException {
contentStream.curveTo(x1, y1, x2, y2, x3, y3);
}
public static void curveTo1(final PDPageContentStream contentStream, float x1, float y1, float x3, float y3) throws IOException {
contentStream.curveTo1(x1, y1, x3, y3);
}
public static void fillNonZero(final PDPageContentStream contentStream) throws IOException {
contentStream.fill();
}
public static void showText(final PDPageContentStream contentStream,
final String text) throws IOException {
contentStream.showText(text);
}
public static PDPageContentStream createAppendablePDPageContentStream(
final PDDocument pdDocument, final PDPage page) throws IOException {
return new PDPageContentStream(pdDocument, page, PDPageContentStream.AppendMode.APPEND, true);
}
public static int getPageRotation(final PDPage page) {
return page.getRotation();
}
public static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color,
LinkStyle linkStyle, final String uri) {
PDAnnotationLink pdLink = createLink(page, rect, color, linkStyle);
PDActionURI actionUri = new PDActionURI();
actionUri.setURI(uri);
pdLink.setAction(actionUri);
return pdLink;
}
public static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color,
LinkStyle linkStyle, final PDDestination destination) {
PDAnnotationLink pdLink = createLink(page, rect, color, linkStyle);
PDActionGoTo gotoAction = new PDActionGoTo();
gotoAction.setDestination(destination);
pdLink.setAction(gotoAction);
return pdLink;
}
/**
* Sets the color in the annotation.
*
* @param annotation the annotation.
* @param color the color to set.
*/
public static void setAnnotationColor(PDAnnotation annotation, Color color) {
annotation.setColor(toPDColor(color));
}
private static PDColor toPDColor(final Color color) {
float[] components = {
color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f
};
return new PDColor(components, PDDeviceRGB.INSTANCE);
}
private static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color,
LinkStyle linkStyle) {
PDAnnotationLink pdLink = new PDAnnotationLink();
if (linkStyle == LinkStyle.none) {
if (noBorder == null) {
noBorder = new PDBorderStyleDictionary();
noBorder.setWidth(0);
}
return pdLink;
}
PDBorderStyleDictionary borderStyle = new PDBorderStyleDictionary();
borderStyle.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE);
pdLink.setBorderStyle(borderStyle);
PDRectangle rotatedRect = transformToPageRotation(rect, page);
pdLink.setRectangle(rotatedRect);
setAnnotationColor(pdLink, color);
return pdLink;
}
/**
* Return the quad points representation of the given rect.
*
* @param rect the rectangle.
* @return the quad points.
*/
public static float[] toQuadPoints(final PDRectangle rect) {
return toQuadPoints(rect, 0, 0);
}
/**
* Return the quad points representation of the given rect.
*
* @param rect the rectangle.
* @param xOffset the offset in x-direction to add.
* @param yOffset the offset in y-direction to add.
* @return the quad points.
*/
private static float[] toQuadPoints(final PDRectangle rect, float xOffset, float yOffset) {
float[] quads = new float[8];
quads[0] = rect.getLowerLeftX() + xOffset; // x1
quads[1] = rect.getUpperRightY() + yOffset; // y1
quads[2] = rect.getUpperRightX() + xOffset; // x2
quads[3] = quads[1]; // y2
quads[4] = quads[0]; // x3
quads[5] = rect.getLowerLeftY() + yOffset; // y3
quads[6] = quads[2]; // x4
quads[7] = quads[5]; // y5
return quads;
}
/**
* Transform the quad points in order to match the page rotation
*
* @param quadPoints the quad points.
* @param page the page.
* @return the transformed quad points.
*/
public static float[] transformToPageRotation(final float[] quadPoints, final PDPage page) {
AffineTransform transform = transformToPageRotation(page);
if (transform == null) {
return quadPoints;
}
float[] rotatedPoints = new float[quadPoints.length];
transform.transform(quadPoints, 0, rotatedPoints, 0, 4);
return rotatedPoints;
}
/**
* Transform the rectangle in order to match the page rotation
*
* @param rect the rectangle.
* @param page the page.
* @return the transformed rectangle.
*/
private static PDRectangle transformToPageRotation(final PDRectangle rect, final PDPage page) {
AffineTransform transform = transformToPageRotation(page);
if (transform == null) {
return rect;
}
float[] points = new float[]{rect.getLowerLeftX(), rect.getLowerLeftY(), rect.getUpperRightX(), rect.getUpperRightY()};
float[] rotatedPoints = new float[4];
transform.transform(points, 0, rotatedPoints, 0, 2);
PDRectangle rotated = new PDRectangle();
rotated.setLowerLeftX(rotatedPoints[0]);
rotated.setLowerLeftY(rotatedPoints[1]);
rotated.setUpperRightX(rotatedPoints[2]);
rotated.setUpperRightY(rotatedPoints[3]);
return rotated;
}
private static AffineTransform transformToPageRotation(final PDPage page) {
int pageRotation = getPageRotation(page);
if (pageRotation == 0) {
return null;
}
float pageWidth = page.getMediaBox().getHeight();
float pageHeight = page.getMediaBox().getWidth();
AffineTransform transform = new AffineTransform();
transform.rotate(pageRotation * Math.PI / 180, pageHeight / 2,
pageWidth / 2);
double offset = Math.abs(pageHeight - pageWidth) / 2;
transform.translate(-offset, offset);
return transform;
}
}

View file

@ -5,8 +5,8 @@ import org.xbib.graphics.pdfbox.layout.util.Enumerators.ArabicEnumerator;
import org.xbib.graphics.pdfbox.layout.util.Enumerators.LowerCaseAlphabeticEnumerator;
import org.xbib.graphics.pdfbox.layout.util.Enumerators.LowerCaseRomanEnumerator;
import org.xbib.graphics.pdfbox.layout.util.Enumerators.RomanEnumerator;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Enumerators are created using this factory. It allows you to register and use
@ -23,7 +23,7 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class EnumeratorFactory {
private final static Map<String, Class<? extends Enumerator>> ENUMERATORS = new ConcurrentHashMap<String, Class<? extends Enumerator>>();
private final static Map<String, Class<? extends Enumerator>> ENUMERATORS = new HashMap<>();
static {
register("1", ArabicEnumerator.class);

View file

@ -23,6 +23,6 @@ public interface WordBreaker {
*/
Pair<String> breakWord(String word,
FontDescriptor fontDescriptor, float maxWidth,
boolean breakHardIfNecessary) throws IOException;
boolean breakHardIfNecessary);
}

View file

@ -2,8 +2,8 @@ package org.xbib.graphics.pdfbox.layout.util;
import org.xbib.graphics.pdfbox.layout.util.WordBreakers.DefaultWordBreaker;
import org.xbib.graphics.pdfbox.layout.util.WordBreakers.NonBreakingWordBreaker;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Factory for creating a {@link WordBreaker}. This may be used to define a
@ -21,12 +21,6 @@ public class WordBreakerFactory {
*/
public final static String WORD_BREAKER_CLASS_PROPERTY = "pdfbox.layout.word.breaker";
/**
* class name of the default word breaker.
*/
public final static String DEFAULT_WORD_BREAKER_CLASS_NAME = DefaultWordBreaker.class
.getName();
/**
* class name of the (legacy) non-breaking word breaker.
*/
@ -34,7 +28,8 @@ public class WordBreakerFactory {
.getName();
private final static WordBreaker DEFAULT_WORD_BREAKER = new DefaultWordBreaker();
private final static Map<String, WordBreaker> WORD_BREAKERS = new ConcurrentHashMap<String, WordBreaker>();
private final static Map<String, WordBreaker> WORD_BREAKERS = new HashMap<>();
/**
* @return the word breaker instance to use.
@ -59,8 +54,7 @@ public class WordBreakerFactory {
try {
return (WordBreaker) Class.forName(className).getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(String.format(
"failed to create word breaker '%s'", className), e);
throw new RuntimeException(String.format("failed to create word breaker '%s'", className), e);
}
}
}

Some files were not shown because too many files have changed in this diff Show more