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 { builder.create {
document { document {
paragraph { 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 { 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;
exports org.xbib.graphics.pdfbox.layout.elements.render; exports org.xbib.graphics.pdfbox.layout.elements.render;
exports org.xbib.graphics.pdfbox.layout.font; exports org.xbib.graphics.pdfbox.layout.font;
exports org.xbib.graphics.pdfbox.layout.shape; 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;
exports org.xbib.graphics.pdfbox.layout.text.annotations; exports org.xbib.graphics.pdfbox.layout.text.annotations;
exports org.xbib.graphics.pdfbox.layout.util; exports org.xbib.graphics.pdfbox.layout.util;
requires transitive org.xbib.graphics.barcode; requires transitive org.xbib.graphics.barcode;
requires transitive org.xbib.graphics.pdfbox; requires transitive org.xbib.graphics.pdfbox;
requires transitive java.desktop; 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, private Entry<Element, LayoutHint> createEntry(final Element element,
final LayoutHint layoutHint) { 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(); Element element = entry.getKey();
LayoutHint layoutHint = entry.getValue(); LayoutHint layoutHint = entry.getValue();
boolean success = false; boolean success = false;
Iterator<Renderer> customRendererIterator = customRenderer.iterator();
// first ask custom renderer to render the element
Iterator<Renderer> customRendererIterator = customRenderer
.iterator();
while (!success && customRendererIterator.hasNext()) { while (!success && customRendererIterator.hasNext()) {
success = customRendererIterator.next().render(renderContext, success = customRendererIterator.next().render(renderContext, element, layoutHint);
element, layoutHint);
} }
// if none of them felt responsible, let the default renderer do the job.
if (!success) { if (!success) {
success = renderContext.render(renderContext, element, success = renderContext.render(renderContext, element, layoutHint);
layoutHint);
} }
if (!success) { if (!success) {
throw new IllegalArgumentException( throw new IllegalArgumentException(String.format(
String.format(
"neither layout %s nor the render context knows what to do with %s", "neither layout %s nor the render context knows what to do with %s",
renderContext.getLayout(), element)); renderContext.getLayout(), element));
} }
} }
renderContext.close(); renderContext.close();
resetPDDocument(); resetPDDocument();
return document; 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 org.xbib.graphics.pdfbox.layout.text.WidthRespecting;
import java.awt.Color; import java.awt.Color;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/** /**
* The frame is a container for a {@link Drawable}, that allows to add margin, * 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 { 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 paddingLeft;
private float paddingRight; 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.TextFlow;
import org.xbib.graphics.pdfbox.layout.text.TextSequenceUtil; import org.xbib.graphics.pdfbox.layout.text.TextSequenceUtil;
import org.xbib.graphics.pdfbox.layout.text.WidthRespecting; 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 * 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 @Override
public void draw(PDDocument pdDocument, PDPageContentStream contentStream, public void draw(PDDocument pdDocument, PDPageContentStream contentStream,
Position upperLeft, DrawListener drawListener) throws IOException { Position upperLeft, DrawListener drawListener) {
drawText(contentStream, upperLeft, getAlignment(), drawListener); drawText(contentStream, upperLeft, getAlignment(), drawListener);
} }
@Override @Override
public Divided divide(float remainingHeight, final float pageHeight) public Divided divide(float remainingHeight, final float pageHeight) {
throws IOException {
return TextSequenceUtil.divide(this, getMaxWidth(), remainingHeight); return TextSequenceUtil.divide(this, getMaxWidth(), remainingHeight);
} }
@Override @Override
public Paragraph removeLeadingEmptyVerticalSpace() throws IOException { public Paragraph removeLeadingEmptyVerticalSpace() {
return removeLeadingEmptyLines(); return removeLeadingEmptyLines();
} }
@Override @Override
public Paragraph removeLeadingEmptyLines() throws IOException { public Paragraph removeLeadingEmptyLines() {
Paragraph result = (Paragraph) super.removeLeadingEmptyLines(); Paragraph result = (Paragraph) super.removeLeadingEmptyLines();
result.setAbsolutePosition(this.getAbsolutePosition()); result.setAbsolutePosition(this.getAbsolutePosition());
result.setAlignment(this.getAlignment()); 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.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.common.PDRectangle; 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.ControlElement;
import org.xbib.graphics.pdfbox.layout.elements.Document; import org.xbib.graphics.pdfbox.layout.elements.Document;
import org.xbib.graphics.pdfbox.layout.elements.Element; 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.DrawListener;
import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationDrawListener; import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationDrawListener;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.io.Closeable; import java.io.Closeable;
import java.io.IOException; 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. * @return <code>true</code> if the page is rotated by 90/270 degrees.
*/ */
public boolean isPageTilted() { public boolean isPageTilted() {
return CompatibilityHelper.getPageRotation(page) == 90 return page.getRotation() == 90 || page.getRotation() == 270;
|| CompatibilityHelper.getPageRotation(page) == 270;
} }
/** /**
@ -374,12 +373,9 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList
if (nextPageFormat != null) { if (nextPageFormat != null) {
setPageFormat(nextPageFormat); setPageFormat(nextPageFormat);
} }
this.page = new PDPage(getPageFormat().getMediaBox()); this.page = new PDPage(getPageFormat().getMediaBox());
this.pdDocument.addPage(page); this.pdDocument.addPage(page);
this.contentStream = CompatibilityHelper this.contentStream = new PDPageContentStream(pdDocument, page, PDPageContentStream.AppendMode.APPEND, true);
.createAppendablePDPageContentStream(pdDocument, page);
// fix orientation // fix orientation
if (getPageOrientation() != getPageFormat().getOrientation()) { if (getPageOrientation() != getPageFormat().getOrientation()) {
if (isPageTilted()) { if (isPageTilted()) {
@ -389,7 +385,7 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList
} }
} }
if (isPageTilted()) { if (isPageTilted()) {
CompatibilityHelper.transform(contentStream, 0, 1, -1, 0, getPageHeight(), 0); contentStream.transform(new Matrix(0, 1, -1, 0, getPageHeight(), 0));
} }
resetPositionToUpperLeft(); resetPositionToUpperLeft();
resetMaxPositionOnPage(); resetMaxPositionOnPage();
@ -410,8 +406,7 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList
document.afterPage(this); document.afterPage(this);
if (getPageFormat().getRotation() != 0) { if (getPageFormat().getRotation() != 0) {
int currentRotation = CompatibilityHelper int currentRotation = getCurrentPage().getRotation();
.getPageRotation(getCurrentPage());
getCurrentPage().setRotation( getCurrentPage().setRotation(
currentRotation + getPageFormat().getRotation()); 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.Alignment;
import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.text.WidthRespecting; import org.xbib.graphics.pdfbox.layout.text.WidthRespecting;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.io.IOException; import java.io.IOException;
/** /**
@ -246,17 +245,13 @@ public class VerticalLayout implements Layout {
break; break;
} }
} }
contentStream.saveGraphicsState(); contentStream.saveGraphicsState();
contentStream.addRect(0, pageFormat.getMarginBottom(), renderContext.getPageWidth(), contentStream.addRect(0, pageFormat.getMarginBottom(), renderContext.getPageWidth(),
renderContext.getHeight()); renderContext.getHeight());
CompatibilityHelper.clip(contentStream); contentStream.clip();
drawable.draw(renderContext.getPdDocument(), contentStream, drawable.draw(renderContext.getPdDocument(), contentStream,
renderContext.getCurrentPosition().add(offsetX, 0), renderContext); renderContext.getCurrentPosition().add(offsetX, 0), renderContext);
contentStream.restoreGraphicsState(); contentStream.restoreGraphicsState();
if (movePosition) { if (movePosition) {
renderContext.movePositionBy(0, -drawable.getHeight()); renderContext.movePositionBy(0, -drawable.getHeight());
} }

View file

@ -12,10 +12,10 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font;
public enum BaseFont implements Font { public enum BaseFont implements Font {
TIMES(PDType1Font.TIMES_ROMAN, PDType1Font.TIMES_BOLD, 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, 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, HELVETICA(PDType1Font.HELVETICA, PDType1Font.HELVETICA_BOLD,
PDType1Font.HELVETICA_OBLIQUE, PDType1Font.HELVETICA_BOLD_OBLIQUE); PDType1Font.HELVETICA_OBLIQUE, PDType1Font.HELVETICA_BOLD_OBLIQUE);
@ -28,8 +28,7 @@ public enum BaseFont implements Font {
private final PDFont boldItalicFont; private final PDFont boldItalicFont;
BaseFont(PDFont plainFont, PDFont boldFont, PDFont italicFont, BaseFont(PDFont plainFont, PDFont boldFont, PDFont italicFont, PDFont boldItalicFont) {
PDFont boldItalicFont) {
this.plainFont = plainFont; this.plainFont = plainFont;
this.boldFont = boldFont; this.boldFont = boldFont;
this.italicFont = italicFont; this.italicFont = italicFont;
@ -37,7 +36,7 @@ public enum BaseFont implements Font {
} }
@Override @Override
public PDFont getPlainFont() { public PDFont getRegularFont() {
return plainFont; return plainFont;
} }

View file

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

View file

@ -1,47 +1,56 @@
package org.xbib.graphics.pdfbox.layout.font; package org.xbib.graphics.pdfbox.layout.font;
import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDFont;
import java.util.Objects;
/** /**
* Container for a Font and size. * Container for a Font and size.
*/ */
public class FontDescriptor { public class FontDescriptor {
/** private final Font font;
* the associated font.
*/
private final PDFont font;
/**
* the font size.
*/
private final float size; private final float size;
/** private final boolean regular;
* Creates the descriptor the the given font and size.
* private final boolean bold;
* @param font the font.
* @param size the size. private final boolean italic;
*/
public FontDescriptor(final PDFont font, final float size) { public FontDescriptor(Font font, float size) {
this.font = font; this(font, size, false, false);
this.size = size;
} }
/** public FontDescriptor(Font font, float size, boolean bold, boolean italic) {
* @return the font. this.font = font;
*/ this.size = size;
public PDFont getFont() { this.regular = !bold && !italic;
this.bold = bold;
this.italic = italic;
}
public Font getFont() {
return font; return font;
} }
/**
* @return the size.
*/
public float getSize() { public float getSize() {
return size; 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 @Override
public String toString() { public String toString() {
return "FontDescriptor [font=" + font + ", size=" + size + "]"; return "FontDescriptor [font=" + font + ", size=" + size + "]";
@ -49,33 +58,14 @@ public class FontDescriptor {
@Override @Override
public int hashCode() { public int hashCode() {
final int prime = 31; return Objects.hash(font, size, regular, bold, italic);
int result = 1;
result = prime * result + ((font == null) ? 0 : font.hashCode());
result = prime * result + Float.floatToIntBits(size);
return result;
} }
@Override @Override
public boolean equals(final Object obj) { public boolean equals(final Object obj) {
if (this == obj) { if (!(obj instanceof FontDescriptor)) {
return true;
}
if (obj == null) {
return false; return false;
} }
if (getClass() != obj.getClass()) { return Objects.hashCode(obj) == Objects.hash(this);
return false;
} }
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.PDFont;
import org.apache.pdfbox.pdmodel.font.PDType0Font; import org.apache.pdfbox.pdmodel.font.PDType0Font;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.Objects; import java.util.Objects;
public class NotoSansFont implements Font { public class NotoSansFont implements Font {
private final PDDocument document;
private static PDType0Font regular; private static PDType0Font regular;
private static PDType0Font bold; private static PDType0Font bold;
@ -16,32 +19,47 @@ public class NotoSansFont implements Font {
private static PDType0Font bolditalic; private static PDType0Font bolditalic;
public NotoSansFont(PDDocument document) throws IOException { public NotoSansFont(PDDocument document) {
if (regular == null) { this.document = document;
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")));
}
} }
@Override @Override
public PDFont getPlainFont() { public PDFont getRegularFont() {
if (regular == null) {
regular = load(document, "NotoSans-Regular.ttf");
}
return regular; return regular;
} }
@Override @Override
public PDFont getBoldFont() { public PDFont getBoldFont() {
if (bold == null) {
bold = load(document, "NotoSans-Bold.ttf");
}
return bold; return bold;
} }
@Override @Override
public PDFont getItalicFont() { public PDFont getItalicFont() {
if (italic == null) {
italic = load(document, "NotoSans-Italic.ttf");
}
return italic; return italic;
} }
@Override @Override
public PDFont getBoldItalicFont() { public PDFont getBoldItalicFont() {
if (bolditalic == null) {
bolditalic = load(document, "NotoSans-BoldItalic.ttf");
}
return bolditalic; 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.apache.pdfbox.pdmodel.PDPageContentStream;
import org.xbib.graphics.pdfbox.layout.text.DrawListener; import org.xbib.graphics.pdfbox.layout.text.DrawListener;
import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.awt.Color; import java.awt.Color;
import java.io.IOException; import java.io.IOException;
/** /**
* Abstract base class for shapes which performs the * Abstract base class for shapes which performs the
* {@link #fill(PDDocument, PDPageContentStream, Position, float, float, Color, DrawListener)} * {@link #fill(PDDocument, PDPageContentStream, Position, float, float, Color, DrawListener)}
* and (@link * and {@link #draw(PDDocument, PDPageContentStream, Position, float, float, Color, Stroke, DrawListener)}
* {@link #draw(PDDocument, PDPageContentStream, Position, float, float, Color, Stroke, DrawListener)}
* .
*/ */
public abstract class AbstractShape implements Shape { public abstract class AbstractShape implements Shape {
@ -21,9 +18,7 @@ public abstract class AbstractShape implements Shape {
public void draw(PDDocument pdDocument, PDPageContentStream contentStream, public void draw(PDDocument pdDocument, PDPageContentStream contentStream,
Position upperLeft, float width, float height, Color color, Position upperLeft, float width, float height, Color color,
Stroke stroke, DrawListener drawListener) throws IOException { Stroke stroke, DrawListener drawListener) throws IOException {
add(pdDocument, contentStream, upperLeft, width, height); add(pdDocument, contentStream, upperLeft, width, height);
if (stroke != null) { if (stroke != null) {
stroke.applyTo(contentStream); stroke.applyTo(contentStream);
} }
@ -31,29 +26,22 @@ public abstract class AbstractShape implements Shape {
contentStream.setStrokingColor(color); contentStream.setStrokingColor(color);
} }
contentStream.stroke(); contentStream.stroke();
if (drawListener != null) { if (drawListener != null) {
drawListener.drawn(this, upperLeft, width, height); drawListener.drawn(this, upperLeft, width, height);
} }
} }
@Override @Override
public void fill(PDDocument pdDocument, PDPageContentStream contentStream, public void fill(PDDocument pdDocument, PDPageContentStream contentStream,
Position upperLeft, float width, float height, Color color, Position upperLeft, float width, float height, Color color,
DrawListener drawListener) throws IOException { DrawListener drawListener) throws IOException {
add(pdDocument, contentStream, upperLeft, width, height); add(pdDocument, contentStream, upperLeft, width, height);
if (color != null) { if (color != null) {
contentStream.setNonStrokingColor(color); contentStream.setNonStrokingColor(color);
} }
CompatibilityHelper.fillNonZero(contentStream); contentStream.fill();
if (drawListener != null) { if (drawListener != null) {
drawListener.drawn(this, upperLeft, width, height); 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.PDDocument;
import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.Position;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.io.IOException; import java.io.IOException;
/** /**
@ -14,6 +13,7 @@ public class RoundRect extends AbstractShape {
private final static float BEZ = 0.551915024494f; private final static float BEZ = 0.551915024494f;
private final float cornerRadiusX; private final float cornerRadiusX;
private final float cornerRadiusY; private final float cornerRadiusY;
/** /**
@ -96,18 +96,16 @@ public class RoundRect extends AbstractShape {
contentStream.moveTo(a.getX(), a.getY()); contentStream.moveTo(a.getX(), a.getY());
addLine(contentStream, a.getX(), a.getY(), b.getX(), b.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()); 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()); 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()); 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()); 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()); g.getY() - bezY, g.getX(), g.getY());
addLine(contentStream, g.getX(), g.getY(), h.getX(), h.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()); a.getY(), a.getX(), a.getY());
} }
@ -120,7 +118,7 @@ public class RoundRect extends AbstractShape {
float y1, float x2, float y2) throws IOException { float y1, float x2, float y2) throws IOException {
float xMid = (x1 + x2) / 2f; float xMid = (x1 + x2) / 2f;
float yMid = (y1 + y2) / 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 org.apache.pdfbox.pdmodel.PDPageContentStream;
import java.io.IOException; 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. * This is a container for all information needed to perform a stroke.
@ -51,6 +53,7 @@ public class Stroke {
public static class DashPattern { public static class DashPattern {
private final float[] pattern; private final float[] pattern;
private final float phase; private final float phase;
/** /**
@ -106,8 +109,11 @@ public class Stroke {
} }
private final CapStyle capStyle; private final CapStyle capStyle;
private final JoinStyle joinStyle; private final JoinStyle joinStyle;
private final DashPattern dashPattern; private final DashPattern dashPattern;
private final float lineWidth; private final float lineWidth;
/** /**
@ -168,18 +174,20 @@ public class Stroke {
* @param contentStream the content stream to apply this stroke to. * @param contentStream the content stream to apply this stroke to.
* @throws IOException by PDFBox. * @throws IOException by PDFBox.
*/ */
public void applyTo(final PDPageContentStream contentStream) public void applyTo(PDPageContentStream contentStream) throws IOException {
throws IOException {
if (getCapStyle() != null) { if (getCapStyle() != null) {
Logger.getLogger("").info(" cap style = " + getCapStyle().value());
contentStream.setLineCapStyle(getCapStyle().value()); contentStream.setLineCapStyle(getCapStyle().value());
} }
if (getJoinStyle() != null) { if (getJoinStyle() != null) {
Logger.getLogger("").info(" join style = " + getJoinStyle().value());
contentStream.setLineJoinStyle(getJoinStyle().value()); contentStream.setLineJoinStyle(getJoinStyle().value());
} }
if (getDashPattern() != null) { if (getDashPattern() != null) {
contentStream.setLineDashPattern(getDashPattern().getPattern(), Logger.getLogger("").info(" dash pattern = " + Arrays.asList(getDashPattern().getPattern()));
getDashPattern().getPhase()); contentStream.setLineDashPattern(getDashPattern().getPattern(), getDashPattern().getPhase());
} }
Logger.getLogger("").info(" line width = " + getLineWidth());
contentStream.setLineWidth(getLineWidth()); contentStream.setLineWidth(getLineWidth());
} }
@ -196,10 +204,14 @@ public class Stroke {
* A builder providing a fluent interface for creating a stroke. * A builder providing a fluent interface for creating a stroke.
*/ */
public static class StrokeBuilder { public static class StrokeBuilder {
private CapStyle capStyle = CapStyle.Cap; private CapStyle capStyle = CapStyle.Cap;
private JoinStyle joinStyle = JoinStyle.Miter; private JoinStyle joinStyle = JoinStyle.Miter;
private DashPattern dashPattern; private DashPattern dashPattern;
float lineWidth = 1f;
private float lineWidth = 1f;
public StrokeBuilder capStyle(CapStyle capStyle) { public StrokeBuilder capStyle(CapStyle capStyle) {
this.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; package org.xbib.graphics.pdfbox.layout.text;
import java.io.IOException;
/** /**
* Defines an area with a width and height. * Defines an area with a width and height.
*/ */
@ -9,13 +7,11 @@ public interface Area {
/** /**
* @return the width of the area. * @return the width of the area.
* @throws IOException by pdfbox
*/ */
float getWidth() throws IOException; float getWidth();
/** /**
* @return the height of the area. * @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; 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 org.xbib.graphics.pdfbox.layout.font.FontDescriptor;
import java.awt.Color; import java.awt.Color;
import java.io.IOException;
/** /**
* A control fragment has no drawable representation but is meant to control the * 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 { public class ControlFragment implements TextFragment {
protected final static FontDescriptor DEFAULT_FONT_DESCRIPTOR = new FontDescriptor( protected final static FontDescriptor DEFAULT_FONT_DESCRIPTOR =
PDType1Font.HELVETICA, 11); new FontDescriptor(BaseFont.HELVETICA, 11);
private String name; private String name;
private final String text; private final String text;
private final FontDescriptor fontDescriptor; private final FontDescriptor fontDescriptor;
private final Color color; private final Color color;
protected ControlFragment(final String text, protected ControlFragment(String text, FontDescriptor fontDescriptor) {
final FontDescriptor fontDescriptor) {
this(null, text, fontDescriptor, Color.black); this(null, text, fontDescriptor, Color.black);
} }
protected ControlFragment(final String name, final String text, protected ControlFragment(String name, final String text,
final FontDescriptor fontDescriptor, final Color color) { FontDescriptor fontDescriptor, final Color color) {
this.name = name; this.name = name;
if (this.name == null) { if (this.name == null) {
this.name = getClass().getSimpleName(); this.name = getClass().getSimpleName();
@ -36,12 +37,12 @@ public class ControlFragment implements TextFragment {
} }
@Override @Override
public float getWidth() throws IOException { public float getWidth() {
return 0; return 0;
} }
@Override @Override
public float getHeight() throws IOException { public float getHeight() {
return getFontDescriptor() == null ? 0 : getFontDescriptor().getSize(); return getFontDescriptor() == null ? 0 : getFontDescriptor().getSize();
} }

View file

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

View file

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

View file

@ -25,10 +25,9 @@ public enum SpaceUnit {
* @return the size in pt. * @return the size in pt.
* @throws IOException by pdfbox * @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) { if (this == em) {
return fontDescriptor.getSize() return fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getAverageFontWidth() / 1000 * size;
* fontDescriptor.getFont().getAverageFontWidth() / 1000 * size;
} }
return size; return size;
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
package org.xbib.graphics.pdfbox.layout.text; package org.xbib.graphics.pdfbox.layout.text;
import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; 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 { public class WrappingNewLine extends NewLine {
/**
* See {@link NewLine#NewLine()}.
*/
public WrappingNewLine() {
super();
}
/** /**
* See {@link NewLine#NewLine(FontDescriptor)}. * See {@link NewLine#NewLine(FontDescriptor)}.
* *
@ -24,15 +16,4 @@ public class WrappingNewLine extends NewLine {
public WrappingNewLine(FontDescriptor fontDescriptor) { public WrappingNewLine(FontDescriptor fontDescriptor) {
super(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<>(); 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. * Creates a styled text.
* *
@ -52,8 +35,7 @@ public class AnnotatedStyledText extends StyledText implements Annotated {
final float rightMargin, final float rightMargin,
final float baselineOffset, final float baselineOffset,
Collection<? extends Annotation> annotations) { Collection<? extends Annotation> annotations) {
super(text, fontDescriptor, color, baselineOffset, leftMargin, super(text, fontDescriptor, color, baselineOffset, leftMargin, rightMargin);
rightMargin);
if (annotations != null) { if (annotations != null) {
this.annotations.addAll(annotations); 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;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation.LinkStyle; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation.LinkStyle;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.UnderlineAnnotation; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.UnderlineAnnotation;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
@ -16,7 +16,7 @@ import java.util.regex.Pattern;
*/ */
public class AnnotationCharacters { public class AnnotationCharacters {
private final static List<AnnotationControlCharacterFactory<?>> FACTORIES = new CopyOnWriteArrayList<>(); private final static List<AnnotationControlCharacterFactory<?>> FACTORIES = new ArrayList<>();
static { static {
register(new HyperlinkControlCharacterFactory()); register(new HyperlinkControlCharacterFactory());
@ -30,8 +30,7 @@ public class AnnotationCharacters {
* *
* @param factory the factory to register. * @param factory the factory to register.
*/ */
public static void register( public static void register(AnnotationControlCharacterFactory<?> factory) {
final AnnotationControlCharacterFactory<?> factory) {
FACTORIES.add(factory); FACTORIES.add(factory);
} }
@ -97,8 +96,7 @@ public class AnnotationCharacters {
@Override @Override
public String unescape(String text) { public String unescape(String text) {
return text return text.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
} }
@Override @Override
@ -119,8 +117,7 @@ public class AnnotationCharacters {
@Override @Override
public UnderlineControlCharacter createControlCharacter(String text, public UnderlineControlCharacter createControlCharacter(String text,
Matcher matcher, final List<CharSequence> charactersSoFar) { Matcher matcher, final List<CharSequence> charactersSoFar) {
return new UnderlineControlCharacter(matcher.group(4), return new UnderlineControlCharacter(matcher.group(4), matcher.group(6));
matcher.group(6));
} }
@Override @Override
@ -130,8 +127,7 @@ public class AnnotationCharacters {
@Override @Override
public String unescape(String text) { public String unescape(String text) {
return text return text.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE);
} }
@Override @Override
@ -149,6 +145,7 @@ public class AnnotationCharacters {
*/ */
public static class HyperlinkControlCharacter extends public static class HyperlinkControlCharacter extends
AnnotationControlCharacter<HyperlinkAnnotation> { AnnotationControlCharacter<HyperlinkAnnotation> {
private HyperlinkAnnotation hyperlink; private HyperlinkAnnotation hyperlink;
protected HyperlinkControlCharacter(final String 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.DrawListener;
import org.xbib.graphics.pdfbox.layout.text.DrawableText; import org.xbib.graphics.pdfbox.layout.text.DrawableText;
import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.Position;
import java.io.IOException;
/** /**
* This listener has to be passed to all * This listener has to be passed to all
@ -44,59 +43,35 @@ public class AnnotationDrawListener implements DrawListener, RenderListener {
return; return;
} }
for (AnnotationProcessor annotationProcessor : annotationProcessors) { for (AnnotationProcessor annotationProcessor : annotationProcessors) {
try {
annotationProcessor.annotatedObjectDrawn( annotationProcessor.annotatedObjectDrawn(
(Annotated) drawnObject, drawContext, upperLeft, width, (Annotated) drawnObject, drawContext, upperLeft, width,
height); height);
} catch (IOException e) {
throw new RuntimeException(
"exception on annotation processing", e);
}
} }
} }
/**
* @throws IOException by pdfbox.
* @deprecated user {@link #afterRender()} instead.
*/
@Deprecated @Deprecated
public void finalizeAnnotations() throws IOException { public void finalizeAnnotations() {
afterRender(); afterRender();
} }
@Override @Override
public void beforePage(RenderContext renderContext) throws IOException { public void beforePage(RenderContext renderContext) {
for (AnnotationProcessor annotationProcessor : annotationProcessors) { for (AnnotationProcessor annotationProcessor : annotationProcessors) {
try {
annotationProcessor.beforePage(drawContext); annotationProcessor.beforePage(drawContext);
} catch (IOException e) {
throw new RuntimeException(
"exception on annotation processing", e);
}
} }
} }
@Override @Override
public void afterPage(RenderContext renderContext) throws IOException { public void afterPage(RenderContext renderContext) {
for (AnnotationProcessor annotationProcessor : annotationProcessors) { for (AnnotationProcessor annotationProcessor : annotationProcessors) {
try {
annotationProcessor.afterPage(drawContext); 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) { for (AnnotationProcessor annotationProcessor : annotationProcessors) {
try {
annotationProcessor.afterRender(drawContext.getPdDocument()); 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.apache.pdfbox.pdmodel.PDDocument;
import org.xbib.graphics.pdfbox.layout.text.DrawContext; import org.xbib.graphics.pdfbox.layout.text.DrawContext;
import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.Position;
import java.io.IOException;
/** /**
* Processes an annotation. * Processes an annotation.
@ -18,34 +17,30 @@ public interface AnnotationProcessor {
* @param upperLeft the upper left position the object has been drawn to. * @param upperLeft the upper left position the object has been drawn to.
* @param width the width of the drawn object. * @param width the width of the drawn object.
* @param height the height of the drawn object. * @param height the height of the drawn object.
* @throws IOException by pdfbox.
*/ */
void annotatedObjectDrawn(final Annotated drawnObject, void annotatedObjectDrawn(final Annotated drawnObject,
final DrawContext drawContext, Position upperLeft, float width, final DrawContext drawContext, Position upperLeft, float width,
float height) throws IOException; float height);
/** /**
* Called before a page is drawn. * Called before a page is drawn.
* *
* @param drawContext the drawing context. * @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. * Called after a page is drawn.
* *
* @param drawContext the drawing context. * @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. * Called after all rendering has been performed.
* *
* @param document the document. * @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.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.common.PDRectangle; 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.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.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination;
import org.xbib.graphics.pdfbox.layout.text.DrawContext; import org.xbib.graphics.pdfbox.layout.text.DrawContext;
import org.xbib.graphics.pdfbox.layout.text.Position; 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.AnchorAnnotation;
import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation; 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.HyperlinkAnnotation.LinkStyle;
import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper;
import java.awt.Color; import java.awt.Color;
import java.awt.geom.AffineTransform;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -26,13 +33,13 @@ import java.util.Map.Entry;
*/ */
public class HyperlinkAnnotationProcessor implements AnnotationProcessor { public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
private final Map<String, PageAnchor> anchorMap = new HashMap<String, PageAnchor>(); private final Map<String, PageAnchor> anchorMap = new HashMap<>();
private final Map<PDPage, List<Hyperlink>> linkMap = new HashMap<PDPage, List<Hyperlink>>(); private final Map<PDPage, List<Hyperlink>> linkMap = new HashMap<>();
@Override @Override
public void annotatedObjectDrawn(Annotated drawnObject, public void annotatedObjectDrawn(Annotated drawnObject,
DrawContext drawContext, Position upperLeft, float width, DrawContext drawContext, Position upperLeft, float width,
float height) throws IOException { float height) {
if (!(drawnObject instanceof AnnotatedStyledText)) { if (!(drawnObject instanceof AnnotatedStyledText)) {
return; return;
@ -85,26 +92,107 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
} }
@Override @Override
public void afterRender(PDDocument document) throws IOException { public void afterRender(PDDocument document) {
for (Entry<PDPage, List<Hyperlink>> entry : linkMap.entrySet()) { for (Entry<PDPage, List<Hyperlink>> entry : linkMap.entrySet()) {
PDPage page = entry.getKey(); PDPage page = entry.getKey();
List<Hyperlink> links = entry.getValue(); List<Hyperlink> links = entry.getValue();
for (Hyperlink hyperlink : links) { for (Hyperlink hyperlink : links) {
PDAnnotationLink pdLink = null; PDAnnotationLink pdLink;
if (hyperlink.getHyperlinkURI().startsWith("#")) { if (hyperlink.getHyperlinkURI().startsWith("#")) {
pdLink = createGotoLink(hyperlink); pdLink = createGotoLink(hyperlink);
} else { } else {
pdLink = CompatibilityHelper.createLink(page, pdLink = createLink(page,
hyperlink.getRect(), hyperlink.getColor(), hyperlink.getRect(), hyperlink.getColor(),
hyperlink.getLinkStyle(), hyperlink.getLinkStyle(),
hyperlink.getHyperlinkURI()); hyperlink.getHyperlinkURI());
} }
try {
page.getAnnotations().add(pdLink); 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) { private PDAnnotationLink createGotoLink(Hyperlink hyperlink) {
String anchor = hyperlink.getHyperlinkURI().substring(1); String anchor = hyperlink.getHyperlinkURI().substring(1);
PageAnchor pageAnchor = anchorMap.get(anchor); PageAnchor pageAnchor = anchorMap.get(anchor);
@ -116,7 +204,7 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
xyzDestination.setPage(pageAnchor.getPage()); xyzDestination.setPage(pageAnchor.getPage());
xyzDestination.setLeft((int) pageAnchor.getX()); xyzDestination.setLeft((int) pageAnchor.getX());
xyzDestination.setTop((int) pageAnchor.getY()); xyzDestination.setTop((int) pageAnchor.getY());
return CompatibilityHelper.createLink(pageAnchor.getPage(), hyperlink.getRect(), return createLink(pageAnchor.getPage(), hyperlink.getRect(),
hyperlink.getColor(), hyperlink.getLinkStyle(), xyzDestination); hyperlink.getColor(), hyperlink.getLinkStyle(), xyzDestination);
} }
@ -147,7 +235,6 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
public String toString() { public String toString() {
return "PageAnchor [page=" + page + ", x=" + x + ", y=" + y + "]"; return "PageAnchor [page=" + page + ", x=" + x + ", y=" + y + "]";
} }
} }
private static class Hyperlink { private static class Hyperlink {
@ -186,7 +273,5 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor {
+ ", hyperlinkUri=" + hyperlinkUri + ", linkStyle=" + ", hyperlinkUri=" + hyperlinkUri + ", linkStyle="
+ 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 org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.UnderlineAnnotation;
import java.awt.Color; import java.awt.Color;
import java.io.IOException; import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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. * the needed hyperlink metadata to the PDF document.
*/ */
public class UnderlineAnnotationProcessor implements AnnotationProcessor { public class UnderlineAnnotationProcessor implements AnnotationProcessor {
@ -22,23 +23,20 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
@Override @Override
public void annotatedObjectDrawn(Annotated drawnObject, public void annotatedObjectDrawn(Annotated drawnObject,
DrawContext drawContext, Position upperLeft, float width, DrawContext drawContext,
Position upperLeft,
float width,
float height) { float height) {
if (!(drawnObject instanceof StyledText)) { if (!(drawnObject instanceof StyledText)) {
return; return;
} }
StyledText drawnText = (StyledText) drawnObject; StyledText drawnText = (StyledText) drawnObject;
for (UnderlineAnnotation underlineAnnotation : drawnObject for (UnderlineAnnotation underlineAnnotation : drawnObject.getAnnotationsOfType(UnderlineAnnotation.class)) {
.getAnnotationsOfType(UnderlineAnnotation.class)) {
float fontSize = drawnText.getFontDescriptor().getSize(); float fontSize = drawnText.getFontDescriptor().getSize();
float ascent = fontSize float ascent = fontSize * drawnText.getFontDescriptor().getSelectedFont().getFontDescriptor().getAscent() / 1000;
* drawnText.getFontDescriptor().getFont()
.getFontDescriptor().getAscent() / 1000;
float baselineOffset = fontSize * underlineAnnotation.getBaselineOffsetScale(); float baselineOffset = fontSize * underlineAnnotation.getBaselineOffsetScale();
float thickness = (0.01f + fontSize * 0.05f) float thickness = (0.01f + fontSize * 0.05f) * underlineAnnotation.getLineWeight();
* underlineAnnotation.getLineWeight(); Position start = new Position(upperLeft.getX(), upperLeft.getY() - ascent + baselineOffset);
Position start = new Position(upperLeft.getX(), upperLeft.getY()
- ascent + baselineOffset);
Position end = new Position(start.getX() + width, start.getY()); Position end = new Position(start.getX() + width, start.getY());
Stroke stroke = Stroke.builder().lineWidth(thickness).build(); Stroke stroke = Stroke.builder().lineWidth(thickness).build();
Line line = new Line(start, end, stroke, drawnText.getColor()); Line line = new Line(start, end, stroke, drawnText.getColor());
@ -47,12 +45,12 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
} }
@Override @Override
public void beforePage(DrawContext drawContext) throws IOException { public void beforePage(DrawContext drawContext) {
linesOnPage.clear(); linesOnPage.clear();
} }
@Override @Override
public void afterPage(DrawContext drawContext) throws IOException { public void afterPage(DrawContext drawContext) {
for (Line line : linesOnPage) { for (Line line : linesOnPage) {
line.draw(drawContext.getCurrentPageContentStream()); line.draw(drawContext.getCurrentPageContentStream());
} }
@ -60,15 +58,18 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
} }
@Override @Override
public void afterRender(PDDocument document) throws IOException { public void afterRender(PDDocument document) {
linesOnPage.clear(); linesOnPage.clear();
} }
private static class Line { private static class Line {
private final Position start; private final Position start;
private final Position end; private final Position end;
private final Stroke stroke; private final Stroke stroke;
private final Color color; private final Color color;
public Line(Position start, Position end, Stroke stroke, Color color) { public Line(Position start, Position end, Stroke stroke, Color color) {
@ -79,7 +80,8 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
this.color = color; this.color = color;
} }
public void draw(PDPageContentStream contentStream) throws IOException { public void draw(PDPageContentStream contentStream) {
try {
if (color != null) { if (color != null) {
contentStream.setStrokingColor(color); contentStream.setStrokingColor(color);
} }
@ -88,6 +90,9 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor {
} }
contentStream.moveTo(start.getX(), start.getY()); contentStream.moveTo(start.getX(), start.getY());
contentStream.lineTo(end.getX(), end.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.LowerCaseAlphabeticEnumerator;
import org.xbib.graphics.pdfbox.layout.util.Enumerators.LowerCaseRomanEnumerator; import org.xbib.graphics.pdfbox.layout.util.Enumerators.LowerCaseRomanEnumerator;
import org.xbib.graphics.pdfbox.layout.util.Enumerators.RomanEnumerator; import org.xbib.graphics.pdfbox.layout.util.Enumerators.RomanEnumerator;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* Enumerators are created using this factory. It allows you to register and use * 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 { 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 { static {
register("1", ArabicEnumerator.class); register("1", ArabicEnumerator.class);

View file

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

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