From 4ad07247b23b5b9e8f00073ce8faa301f38d57d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Tue, 2 Mar 2021 14:24:31 +0100 Subject: [PATCH] adding table, boxable, better font descriptor with TTF fonts --- .../pdfbox/groovy/test/BaseBuilderSpec.groovy | 2 +- .../src/main/java/module-info.java | 4 + .../pdfbox/layout/boxable/BaseTable.java | 26 + .../graphics/pdfbox/layout/boxable/Cell.java | 707 ++++++++++++++ .../layout/boxable/DefaultPageProvider.java | 62 ++ .../pdfbox/layout/boxable/FontMetrics.java | 28 + .../pdfbox/layout/boxable/FontUtils.java | 133 +++ .../pdfbox/layout/boxable/HTMLListNode.java | 47 + .../layout/boxable/HorizontalAlignment.java | 18 + .../graphics/pdfbox/layout/boxable/Image.java | 127 +++ .../pdfbox/layout/boxable/ImageCell.java | 56 ++ .../pdfbox/layout/boxable/ImageUtils.java | 69 ++ .../pdfbox/layout/boxable/LineStyle.java | 122 +++ .../pdfbox/layout/boxable/PDStreamUtils.java | 112 +++ .../boxable/PageContentStreamOptimized.java | 174 ++++ .../pdfbox/layout/boxable/PageProvider.java | 15 + .../pdfbox/layout/boxable/Paragraph.java | 740 ++++++++++++++ .../pdfbox/layout/boxable/PipelineLayer.java | 134 +++ .../graphics/pdfbox/layout/boxable/Row.java | 262 +++++ .../graphics/pdfbox/layout/boxable/Table.java | 917 ++++++++++++++++++ .../pdfbox/layout/boxable/TableCell.java | 339 +++++++ .../pdfbox/layout/boxable/TextToken.java | 25 + .../pdfbox/layout/boxable/TextType.java | 5 + .../graphics/pdfbox/layout/boxable/Token.java | 56 ++ .../pdfbox/layout/boxable/TokenType.java | 5 + .../pdfbox/layout/boxable/Tokenizer.java | 304 ++++++ .../layout/boxable/VerticalAlignment.java | 18 + .../layout/boxable/WrappingFunction.java | 6 + .../pdfbox/layout/elements/Document.java | 21 +- .../pdfbox/layout/elements/Frame.java | 4 +- .../pdfbox/layout/elements/Paragraph.java | 10 +- .../layout/elements/render/RenderContext.java | 15 +- .../elements/render/VerticalLayout.java | 7 +- .../graphics/pdfbox/layout/font/BaseFont.java | 9 +- .../graphics/pdfbox/layout/font/Font.java | 2 +- .../pdfbox/layout/font/FontDescriptor.java | 80 +- .../pdfbox/layout/font/NotoSansFont.java | 34 +- .../pdfbox/layout/shape/AbstractShape.java | 16 +- .../pdfbox/layout/shape/RoundRect.java | 14 +- .../graphics/pdfbox/layout/shape/Stroke.java | 22 +- .../pdfbox/layout/table/AbstractCell.java | 329 +++++++ .../pdfbox/layout/table/AbstractTextCell.java | 100 ++ .../pdfbox/layout/table/BorderStyle.java | 25 + .../layout/table/BorderStyleInterface.java | 10 + .../graphics/pdfbox/layout/table/Column.java | 45 + ...CouldNotDetermineStringWidthException.java | 9 + .../layout/table/HorizontalAlignment.java | 7 + .../pdfbox/layout/table/Hyperlink.java | 84 ++ .../pdfbox/layout/table/ImageCell.java | 92 ++ .../graphics/pdfbox/layout/table/Markup.java | 58 ++ .../graphics/pdfbox/layout/table/NewLine.java | 23 + .../pdfbox/layout/table/ParagraphCell.java | 134 +++ .../layout/table/ParagraphProcessor.java | 10 + .../graphics/pdfbox/layout/table/PdfUtil.java | 165 ++++ .../table/RepeatedHeaderTableRenderer.java | 76 ++ .../graphics/pdfbox/layout/table/Row.java | 180 ++++ .../layout/table/RowIsTooHighException.java | 10 + .../pdfbox/layout/table/Settings.java | 321 ++++++ .../pdfbox/layout/table/StyledText.java | 66 ++ .../graphics/pdfbox/layout/table/Table.java | 371 +++++++ .../table/TableNotYetBuiltException.java | 5 + .../pdfbox/layout/table/TableRenderer.java | 246 +++++ .../layout/table/TableSetupException.java | 9 + .../pdfbox/layout/table/TextCell.java | 193 ++++ .../layout/table/VerticalAlignment.java | 7 + .../pdfbox/layout/table/VerticalTextCell.java | 18 + .../table/render/AbstractCellRenderer.java | 138 +++ .../table/render/ImageCellRenderer.java | 43 + .../table/render/PageNotSetException.java | 9 + .../table/render/ParagraphCellRenderer.java | 81 ++ .../layout/table/render/PositionedLine.java | 87 ++ .../table/render/PositionedRectangle.java | 48 + .../table/render/PositionedStyledText.java | 53 + .../layout/table/render/RenderContext.java | 33 + .../layout/table/render/RenderUtil.java | 55 ++ .../pdfbox/layout/table/render/Renderer.java | 11 + .../layout/table/render/TextCellRenderer.java | 88 ++ .../render/VerticalTextCellRenderer.java | 78 ++ .../graphics/pdfbox/layout/text/Area.java | 8 +- .../pdfbox/layout/text/ControlFragment.java | 21 +- .../graphics/pdfbox/layout/text/Indent.java | 124 +-- .../pdfbox/layout/text/IndentCharacters.java | 39 +- .../graphics/pdfbox/layout/text/NewLine.java | 22 +- .../pdfbox/layout/text/SpaceUnit.java | 5 +- .../pdfbox/layout/text/StyledText.java | 79 +- .../graphics/pdfbox/layout/text/TextFlow.java | 41 +- .../pdfbox/layout/text/TextFlowUtil.java | 118 +-- .../graphics/pdfbox/layout/text/TextLine.java | 130 +-- .../pdfbox/layout/text/TextSequenceUtil.java | 90 +- .../pdfbox/layout/text/WrappingNewLine.java | 19 - .../text/annotations/AnnotatedStyledText.java | 20 +- .../annotations/AnnotationCharacters.java | 17 +- .../annotations/AnnotationDrawListener.java | 45 +- .../text/annotations/AnnotationProcessor.java | 13 +- .../HyperlinkAnnotationProcessor.java | 109 ++- .../UnderlineAnnotationProcessor.java | 49 +- .../layout/util/CompatibilityHelper.java | 229 ----- .../pdfbox/layout/util/EnumeratorFactory.java | 4 +- .../pdfbox/layout/util/WordBreaker.java | 2 +- .../layout/util/WordBreakerFactory.java | 14 +- .../pdfbox/layout/util/WordBreakers.java | 24 +- .../layout/test/CustomAnnotationTest.java | 114 ++- .../pdfbox/layout/test/CustomRenderer.java | 17 +- .../pdfbox/layout/test/IndentationTest.java | 116 ++- .../graphics/pdfbox/layout/test/Listener.java | 12 +- .../pdfbox/layout/test/LowLevelText.java | 8 +- .../pdfbox/layout/test/MarkupTest.java | 26 +- .../pdfbox/layout/test/table/TableTest.java | 172 ++++ .../io/vector/pdf/util/Resources.java | 23 +- 109 files changed, 8698 insertions(+), 1076 deletions(-) create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/BaseTable.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Cell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/DefaultPageProvider.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/FontMetrics.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/FontUtils.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/HTMLListNode.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/HorizontalAlignment.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Image.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/ImageCell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/ImageUtils.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/LineStyle.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PDStreamUtils.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PageContentStreamOptimized.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PageProvider.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Paragraph.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PipelineLayer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Row.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Table.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TableCell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TextToken.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TextType.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Token.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TokenType.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Tokenizer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/VerticalAlignment.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/WrappingFunction.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/AbstractCell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/AbstractTextCell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/BorderStyle.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/BorderStyleInterface.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Column.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/CouldNotDetermineStringWidthException.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/HorizontalAlignment.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Hyperlink.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ImageCell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Markup.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/NewLine.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ParagraphCell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ParagraphProcessor.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/PdfUtil.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/RepeatedHeaderTableRenderer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Row.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/RowIsTooHighException.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Settings.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/StyledText.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Table.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableNotYetBuiltException.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableRenderer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableSetupException.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TextCell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/VerticalAlignment.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/VerticalTextCell.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/AbstractCellRenderer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/ImageCellRenderer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PageNotSetException.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/ParagraphCellRenderer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedLine.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedRectangle.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedStyledText.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/RenderContext.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/RenderUtil.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/Renderer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/TextCellRenderer.java create mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/VerticalTextCellRenderer.java delete mode 100644 graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/CompatibilityHelper.java create mode 100644 graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/table/TableTest.java diff --git a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BaseBuilderSpec.groovy b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BaseBuilderSpec.groovy index d48abf4..0004569 100644 --- a/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BaseBuilderSpec.groovy +++ b/graphics-pdfbox-groovy/src/test/groovy/org/xbib/graphics/pdfbox/groovy/test/BaseBuilderSpec.groovy @@ -228,7 +228,7 @@ abstract class BaseBuilderSpec extends Specification { builder.create { document { paragraph { - barcode(height: 150.px, width: 500.px, value: '12345678') + barcode(height: 150.px, width: 500.px, value: '12345678', type: 'CODE39') } } } diff --git a/graphics-pdfbox-layout/src/main/java/module-info.java b/graphics-pdfbox-layout/src/main/java/module-info.java index cad745f..90c15f7 100644 --- a/graphics-pdfbox-layout/src/main/java/module-info.java +++ b/graphics-pdfbox-layout/src/main/java/module-info.java @@ -1,12 +1,16 @@ module org.xbib.graphics.layout.pdfbox { + exports org.xbib.graphics.pdfbox.layout.boxable; exports org.xbib.graphics.pdfbox.layout.elements; exports org.xbib.graphics.pdfbox.layout.elements.render; exports org.xbib.graphics.pdfbox.layout.font; exports org.xbib.graphics.pdfbox.layout.shape; + exports org.xbib.graphics.pdfbox.layout.table; + exports org.xbib.graphics.pdfbox.layout.table.render; exports org.xbib.graphics.pdfbox.layout.text; exports org.xbib.graphics.pdfbox.layout.text.annotations; exports org.xbib.graphics.pdfbox.layout.util; requires transitive org.xbib.graphics.barcode; requires transitive org.xbib.graphics.pdfbox; requires transitive java.desktop; + requires java.logging; } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/BaseTable.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/BaseTable.java new file mode 100644 index 0000000..97b94d9 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/BaseTable.java @@ -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 { + + 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 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 + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Cell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Cell.java new file mode 100644 index 0000000..8c1f3f9 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Cell.java @@ -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 { + + 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 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; + + /** + *

+ * Constructs a cell with the default alignment + * {@link VerticalAlignment#TOP} {@link HorizontalAlignment#LEFT}. + *

+ * + * @param row + * @param width + * @param text + * @param isCalculated + * @see Cell#Cell(Row, float, String, boolean, HorizontalAlignment, + * VerticalAlignment) + */ + Cell(Row 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 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; + } + + /** + *

+ * Sets cell's text {@link Color}. + *

+ * + * @param textColor designated text {@link Color} + */ + public void setTextColor(Color textColor) { + this.textColor = textColor; + } + + /** + *

+ * Gets fill (background) {@link Color} for the current cell. + *

+ * + * @return Fill {@link Color} for the cell + */ + public Color getFillColor() { + return fillColor; + } + + /** + *

+ * Sets fill (background) {@link Color} for the current cell. + *

+ * + * @param fillColor Fill {@link Color} for the cell + */ + public void setFillColor(Color fillColor) { + this.fillColor = fillColor; + } + + /** + *

+ * Gets cell's width. + *

+ * + * @return Cell's width + */ + public float getWidth() { + return width; + } + + /** + *

+ * 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()); + } + + /** + *

+ * 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()); + } + + /** + *

+ * Retrieves text from current cell + *

+ * + * @return cell's text + */ + public String getText() { + return text; + } + + /** + *

+ * Sets cell's text value + *

+ * + * @param text Text value of the cell + */ + public void setText(String text) { + this.text = text; + + // paragraph invalidated + paragraph = null; + } + + /** + *

+ * Gets appropriate {@link PDFont} for current cell. + *

+ * + * @return {@link PDFont} for current cell + * @throws IllegalArgumentException if font is not set. + */ + public PDFont getFont() { + if (font == null) { + throw new IllegalArgumentException("Font not set."); + } + if (isHeaderCell) { + return fontBold; + } else { + return font; + } + } + + /** + *

+ * Sets appropriate {@link PDFont} for current cell. + *

+ * + * @param font {@link PDFont} for current cell + */ + public void setFont(PDFont font) { + this.font = font; + + // paragraph invalidated + paragraph = null; + } + + /** + *

+ * Gets {@link PDFont} size for current cell (in points). + *

+ * + * @return {@link PDFont} size for current cell (in points). + */ + public float getFontSize() { + return fontSize; + } + + /** + *

+ * Sets {@link PDFont} size for current cell (in points). + *

+ * + * @param fontSize {@link PDFont} size for current cell (in points). + */ + public void setFontSize(float fontSize) { + this.fontSize = fontSize; + + // paragraph invalidated + paragraph = null; + } + + /** + *

+ * Retrieves a valid {@link Paragraph} depending of cell's {@link PDFont} + * and value rotation. + *

+ * + *

+ * If cell has rotated value then {@link Paragraph} width is depending of + * {@link Cell#getInnerHeight()} otherwise {@link Cell#getInnerWidth()} + *

+ * + * @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(); + } + + /** + *

+ * Gets the cell's height according to {@link Row}'s height + *

+ * + * @return {@link Row}'s height + */ + public float getHeight() { + return row.getHeight(); + } + + /** + *

+ * Gets the height of the single cell, opposed to {@link #getHeight()}, + * which returns the row's height. + *

+ *

+ * Depending of rotated/normal cell's value there is two cases for + * calculation: + *

+ *
    + *
  1. Rotated value - cell's height is equal to overall text length in the + * cell with necessery paddings (top,bottom)
  2. + *
  3. Normal value - cell's height is equal to {@link Paragraph}'s height + * with necessery paddings (top,bottom)
  4. + *
+ * + * @return Cell's height + * @throws IllegalStateException if font 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()); + } + } + + /** + *

+ * Sets the height of the single cell. + *

+ * + * @param height Cell's height + */ + public void setHeight(final Float height) { + this.height = height; + } + + /** + *

+ * Gets {@link Paragraph}'s height + *

+ * + * @return {@link Paragraph}'s height + */ + public float getTextHeight() { + return getParagraph().getHeight(); + } + + /** + *

+ * Gets {@link Paragraph}'s width + *

+ * + * @return {@link Paragraph}'s width + */ + public float getTextWidth() { + return getParagraph().getWidth(); + } + + /** + *

+ * Gets cell's left padding (in points). + *

+ * + * @return Cell's left padding (in points). + */ + public float getLeftPadding() { + return leftPadding; + } + + /** + *

+ * Sets cell's left padding (in points) + *

+ * + * @param cellLeftPadding Cell's left padding (in points). + */ + public void setLeftPadding(float cellLeftPadding) { + this.leftPadding = cellLeftPadding; + + // paragraph invalidated + paragraph = null; + } + + /** + *

+ * Gets cell's right padding (in points). + *

+ * + * @return Cell's right padding (in points). + */ + public float getRightPadding() { + return rightPadding; + } + + /** + *

+ * Sets cell's right padding (in points) + *

+ * + * @param cellRightPadding Cell's right padding (in points). + */ + public void setRightPadding(float cellRightPadding) { + this.rightPadding = cellRightPadding; + + // paragraph invalidated + paragraph = null; + } + + /** + *

+ * Gets cell's top padding (in points). + *

+ * + * @return Cell's top padding (in points). + */ + public float getTopPadding() { + return topPadding; + } + + /** + *

+ * Sets cell's top padding (in points) + *

+ * + * @param cellTopPadding Cell's top padding (in points). + */ + public void setTopPadding(float cellTopPadding) { + this.topPadding = cellTopPadding; + } + + /** + *

+ * Gets cell's bottom padding (in points). + *

+ * + * @return Cell's bottom padding (in points). + */ + public float getBottomPadding() { + return bottomPadding; + } + + /** + *

+ * Sets cell's bottom padding (in points) + *

+ * + * @param cellBottomPadding Cell's bottom padding (in points). + */ + public void setBottomPadding(float cellBottomPadding) { + this.bottomPadding = cellBottomPadding; + } + + /** + *

+ * Gets free vertical space of cell. + *

+ * + *

+ * 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}. + *

+ * + * @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(); + } + } + + /** + *

+ * Gets free horizontal space of cell. + *

+ * + *

+ * 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}. + *

+ * + * @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; + } + + /** + *

+ * 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; + } + + /** + *

+ * Sets the {@linkplain PDFont font} used for bold text, for example in + * {@linkplain #isHeaderCell() header cells}. + *

+ * + * @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; + } + + /** + *

+ * Copies the style of an existing cell to this cell + *

+ * + * @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()); + } + + /** + *

+ * Compares the style of a cell with another cell + *

+ * + * @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; + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/DefaultPageProvider.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/DefaultPageProvider.java new file mode 100644 index 0000000..7bbf844 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/DefaultPageProvider.java @@ -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 { + + 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); + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/FontMetrics.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/FontMetrics.java new file mode 100644 index 0000000..b27b103 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/FontMetrics.java @@ -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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/FontUtils.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/FontUtils.java new file mode 100644 index 0000000..4055249 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/FontUtils.java @@ -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 fontMetrics = new HashMap<>(); + + private static final Map 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); + } + } + + /** + *

+ * Calculate the font ascent distance. + *

+ * + * @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; + } + + /** + *

+ * Calculate the font descent distance. + *

+ * + * @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; + } + + /** + *

+ * Calculate the font height. + *

+ * + * @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; + } + + /** + *

+ * Create basic {@link FontMetrics} for current font. + *

+ * + * @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 getDefaultfonts() { + return defaultFonts; + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/HTMLListNode.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/HTMLListNode.java new file mode 100644 index 0000000..c6c99a5 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/HTMLListNode.java @@ -0,0 +1,47 @@ +package org.xbib.graphics.pdfbox.layout.boxable; + +/** + *

+ * Data container for HTML ordered list elements. + *

+ * + * @author hstimac + */ +public class HTMLListNode { + + /** + *

+ * Element's current ordering number (e.g third element in the current list) + *

+ */ + private int orderingNumber; + + /** + *

+ * Element's whole ordering number value (e.g 1.1.2.1) + *

+ */ + 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; + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/HorizontalAlignment.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/HorizontalAlignment.java new file mode 100644 index 0000000..51590e8 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/HorizontalAlignment.java @@ -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; + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Image.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Image.java new file mode 100644 index 0000000..ae374db --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Image.java @@ -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}; + + /** + *

+ * Constructor for default images + *

+ * + * @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(); + } + + /** + *

+ * Drawing simple {@link Image} in {@link PDPageContentStream}. + *

+ * + * @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); + } + + /** + *

+ * Method which scale {@link Image} with designated width + *

+ * + * @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)); + } + + /** + *

+ * 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; + } + + /** + *

+ * 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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/ImageCell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/ImageCell.java new file mode 100644 index 0000000..c678142 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/ImageCell.java @@ -0,0 +1,56 @@ +package org.xbib.graphics.pdfbox.layout.boxable; + +import org.apache.pdfbox.pdmodel.PDPage; + +public class ImageCell extends Cell { + + private Image img; + + private final HorizontalAlignment align; + + private final VerticalAlignment valign; + + ImageCell(Row 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 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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/ImageUtils.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/ImageUtils.java new file mode 100644 index 0000000..515bf3f --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/ImageUtils.java @@ -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; + +/** + *

+ * Utility methods for images + *

+ * + * @author mkuehne + * @author hstimac + */ +public class ImageUtils { + + // utility class, no instance needed + private ImageUtils() { + } + + /** + *

+ * Simple reading image from file + *

+ * + * @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); + } + + /** + *

+ * Provide an ability to scale {@link Image} on desired {@link Dimension} + *

+ * + * @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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/LineStyle.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/LineStyle.java new file mode 100644 index 0000000..5f65b39 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/LineStyle.java @@ -0,0 +1,122 @@ +package org.xbib.graphics.pdfbox.layout.boxable; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.util.Objects; + +/** + *

+ * The LineStyle class defines a basic set of rendering attributes + * for lines. + *

+ */ +public class LineStyle { + + private final Color color; + + private final float width; + + private float[] dashArray; + + private float dashPhase; + + /** + *

+ * Simple constructor for setting line {@link Color} and line width + *

+ * + * @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; + } + + /** + *

+ * Provides ability to produce dotted line. + *

+ * + * @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; + } + + /** + *

+ * Provides ability to produce dashed line. + *

+ * + * @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); + } + + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PDStreamUtils.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PDStreamUtils.java new file mode 100644 index 0000000..7a3a0a0 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PDStreamUtils.java @@ -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; + +/** + *

+ * Utility methods for {@link PDPageContentStream} + *

+ */ +public final class PDStreamUtils { + + private PDStreamUtils() { + } + + /** + *

+ * Provides ability to write on a {@link PDPageContentStream}. The text will + * be written above Y coordinate. + *

+ * + * @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); + } + } + + /** + *

+ * Provides ability to draw rectangle for debugging purposes. + *

+ * + * @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); + } + } + + /** + *

+ * Provides ability to draw font metrics (font height, font ascent, font + * descent). + *

+ * + * @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); + } + + /** + *

+ * Provides ability to set different line styles (line width, dotted line, + * dashed line) + *

+ * + * @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); + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PageContentStreamOptimized.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PageContentStreamOptimized.java new file mode 100644 index 0000000..11f261c --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PageContentStreamOptimized.java @@ -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(); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PageProvider.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PageProvider.java new file mode 100644 index 0000000..90204b8 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PageProvider.java @@ -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 createPage(); + + T nextPage(); + + T previousPage(); + + PDDocument getDocument(); +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Paragraph.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Paragraph.java new file mode 100644 index 0000000..71904d1 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Paragraph.java @@ -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 lineWidths = new HashMap<>(); + private final Map> mapLineTokens = new LinkedHashMap<>(); + private float maxLineWidth = Integer.MIN_VALUE; + private List tokens; + private List 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 getLines() { + // memoize this function because it is very expensive + if (lines != null) { + return lines; + } + + final List 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 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
  • tag already done that + if (listLevel == 0) { + result.add(" "); + lineWidths.put(lineCounter, 0.0f); + mapLineTokens.put(lineCounter, new ArrayList()); + 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
  • mimic
    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()); + 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
  • mimic
    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
  • mimic
    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()); + 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
  • mimic
    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> getMapLineTokens() { + return mapLineTokens; + } + + public float getLineSpacing() { + return lineSpacing; + } + + public void setLineSpacing(float lineSpacing) { + this.lineSpacing = lineSpacing; + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PipelineLayer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PipelineLayer.java new file mode 100644 index 0000000..a258529 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/PipelineLayer.java @@ -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 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 tokens() { + return new ArrayList<>(tokens); + } + + @Override + public String toString() { + return text.toString() + "(" + lastTextToken + ") [width: " + width() + ", trimmed: " + trimmedWidth() + "]"; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Row.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Row.java new file mode 100644 index 0000000..7a53018 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Row.java @@ -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 { + + private final Table table; + PDOutlineItem bookmark; + List> cells; + private boolean headerRow = false; + float height; + private float lineSpacing = 1; + + Row(Table table, List> cells, float height) { + this.table = table; + this.cells = cells; + this.height = height; + } + + Row(Table table, float height) { + this.table = table; + this.cells = new ArrayList<>(); + this.height = height; + } + + /** + *

    + * Creates a cell with provided width, cell value and default left top + * alignment + *

    + * + * @param width Absolute width in points or in % of table width + * @param value Cell's value (content) + * @return New {@link Cell} + */ + public Cell createCell(float width, String value) { + Cell cell = new Cell(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; + } + + /** + *

    + * Creates an image cell with provided width and {@link Image} + *

    + * + * @param width Cell's width + * @param img {@link Image} in the cell + * @return {@link ImageCell} + */ + public ImageCell createImageCell(float width, Image img) { + ImageCell cell = new ImageCell<>(this, width, img, true); + setBorders(cell, cells.isEmpty()); + cells.add(cell); + return cell; + } + + public Cell createImageCell(float width, Image img, HorizontalAlignment align, VerticalAlignment valign) { + Cell cell = new ImageCell(this, width, img, true, align, valign); + setBorders(cell, cells.isEmpty()); + cells.add(cell); + return cell; + } + + /** + *

    + * Creates a table cell with provided width and table data + *

    + * + * @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 createTableCell(float width, String tableData, PDDocument doc, PDPage page, float yStart, + float pageTopMargin, float pageBottomMargin) throws IOException { + TableCell cell = new TableCell(this, width, tableData, true, doc, page, yStart, pageTopMargin, + pageBottomMargin); + setBorders(cell, cells.isEmpty()); + cells.add(cell); + return cell; + } + + /** + *

    + * Creates a cell with provided width, cell value, horizontal and vertical + * alignment + *

    + * + * @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 createCell(float width, String value, HorizontalAlignment align, VerticalAlignment valign) { + Cell cell = new Cell(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; + } + + /** + *

    + * Creates a cell with the same width as the corresponding header cell + *

    + * + * @param value Cell's value (content) + * @return new {@link Cell} + */ + public Cell createCell(String value) { + float headerCellWidth = table.getHeader().getCells().get(cells.size()).getWidth(); + Cell cell = new Cell(this, headerCellWidth, value, false); + setBorders(cell, cells.isEmpty()); + cells.add(cell); + return cell; + } + + /** + *

    + * Remove left border to avoid double borders from previous cell's right + * border. In most cases left border will be removed. + *

    + * + * @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 cell, final boolean leftBorder) { + if (!leftBorder) { + cell.setLeftBorderStyle(null); + } + } + + /** + *

    + * remove top borders of cells to avoid double borders from cells in + * previous row + *

    + */ + void removeTopBorders() { + for (final Cell cell : cells) { + cell.setTopBorderStyle(null); + } + } + + /** + *

    + * Remove all borders of cells. + *

    + */ + void removeAllBorders() { + for (final Cell cell : cells) { + cell.setBorderStyle(null); + } + } + + /** + *

    + * Gets maximal height of the cells in current row therefore row's height. + *

    + * + * @return Row's height + */ + public float getHeight() { + float maxheight = 0.0f; + for (Cell 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> getCells() { + return cells; + } + + public int getColCount() { + return cells.size(); + } + + public void setCells(List> 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 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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Table.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Table.java new file mode 100644 index 0000000..87b6b1d --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Table.java @@ -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 { + + public final PDDocument document; + private final float margin; + + private T currentPage; + private PageContentStreamOptimized tableContentStream; + private List bookmarks; + private final List> header = new ArrayList<>(); + private final List> 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 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 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 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 createRow(float height) { + Row row = new Row(this, height); + row.setLineSpacing(lineSpacing); + this.rows.add(row); + return row; + } + + public Row createRow(List> cells, float height) { + Row row = new Row(this, cells, height); + row.setLineSpacing(lineSpacing); + this.rows.add(row); + return row; + } + + /** + *

    + * Draws table + *

    + * + * @return Y position of the table + * @throws IOException if underlying stream has problem being written to. + */ + public float draw() throws IOException { + ensureStreamIsOpen(); + + for (Row 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 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 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); + } + } + + /** + *

    + * Method to switch between the {@link PageProvider} and the abstract method + * {@link Table#createPage()}, preferring the {@link PageProvider}. + *

    + *

    + * Will be removed once {@link #createPage()} is removed. + *

    + * + * @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 row, float rowHeight) throws IOException { + + // position into first cell (horizontal) + float cursorX = margin; + float cursorY; + + for (Cell 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 imageCell = (ImageCell) 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 tableCell = (TableCell) 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> 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 row, float rowHeight) throws IOException { + float xStart = margin; + + Iterator> cellIterator = row.getCells().iterator(); + while (cellIterator.hasNext()) { + Cell 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 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 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 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 header) { + this.header.clear(); + addHeaderRow(header); + } + + /** + *

    + * Calculate height of all table cells (essentially, table height). + *

    + *

    + * IMPORTANT: Doesn't acknowledge possible page break. Use with caution. + *

    + * + * @return {@link Table}'s height + */ + public float getHeaderAndDataHeight() { + float height = 0; + for (Row row : rows) { + height += row.getHeight(); + } + return height; + } + + /** + *

    + * Calculates minimum table height that needs to be drawn (all header rows + + * first data row heights). + *

    + * + * @return height + */ + public float getMinimumHeight() { + float height = 0.0f; + int firstDataRowIndex = 0; + if (!header.isEmpty()) { + for (Row headerRow : header) { + // count all header rows height + height += headerRow.getHeight(); + firstDataRowIndex++; + } + } + + if (rows.size() > firstDataRowIndex) { + height += rows.get(firstDataRowIndex).getHeight(); + } + + return height; + } + + /** + *

    + * Setting current row as table header row + *

    + * + * @param row The row that would be added as table's header row + */ + public void addHeaderRow(Row row) { + this.header.add(row); + row.setHeaderRow(true); + } + + /** + *

    + * Retrieves last table's header row + *

    + * + * @return header row + */ + public Row 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> 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; + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TableCell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TableCell.java new file mode 100644 index 0000000..1671bbe --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TableCell.java @@ -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 extends Cell { + + 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 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 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(); + } + + /** + *

    + * 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. + *

    + *

    + * NOTE: if entire row is not header row then use bold instead header cell ( + * {@code + * + * }) + *

    + */ + 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(""); + 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(); + + } + + /** + *

    + * Method provides writing or height calculation of possible outer text + *

    + * + * @param paragraph Paragraph that needs to be written or whose height needs to be + * calculated + * @param onlyCalculateHeight if true 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> 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; + } + + /** + *

    + * This method draw table cell with proper X,Y position which are determined + * in {@link Table#draw()} method + *

    + *

    + * NOTE: if entire row is not header row then use bold instead header cell ( + * {@code + * + * }) + *

    + * + * @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(""); + 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; + } + +} \ No newline at end of file diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TextToken.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TextToken.java new file mode 100644 index 0000000..c5061b3 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TextToken.java @@ -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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TextType.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TextType.java new file mode 100644 index 0000000..62c5d1f --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TextType.java @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.layout.boxable; + +public enum TextType { + HIGHLIGHT, UNDERLINE, SQUIGGLY, STRIKEOUT +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Token.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Token.java new file mode 100644 index 0000000..c42b805 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Token.java @@ -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); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TokenType.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TokenType.java new file mode 100644 index 0000000..0c458e9 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/TokenType.java @@ -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 +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Tokenizer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Tokenizer.java new file mode 100644 index 0000000..1ddeb6d --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/Tokenizer.java @@ -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 findWrapPoints(String text) { + Stack 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 findWrapPointsWithFunction(String text, WrappingFunction wrappingFunction) { + final String[] split = wrappingFunction.getLines(text); + int textIndex = text.length(); + final Stack 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 tokenize(final String text, final WrappingFunction wrappingFunction) { + final List tokens = new ArrayList<>(); + if (text != null) { + final Stack 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) { + // + 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) { + // + 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) { + //
    + 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
    + textIndex += 3; + consumed = true; + } else if (textIndex < text.length() - 4) { + //
    + 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
    + 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
    + textIndex += 5; + consumed = true; + } + } + } + } + } else if ('p' == lookahead1 && '>' == lookahead2) { + //

    + 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) { + //

      + 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) { + //
        + 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) { + //
      • + 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) { + // + 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) { + // + 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) { + //

        + 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) { + //
    + 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) { + // + 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) { + //
  • + 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(); + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/VerticalAlignment.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/VerticalAlignment.java new file mode 100644 index 0000000..d30801f --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/VerticalAlignment.java @@ -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; + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/WrappingFunction.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/WrappingFunction.java new file mode 100644 index 0000000..b9366fc --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/boxable/WrappingFunction.java @@ -0,0 +1,6 @@ +package org.xbib.graphics.pdfbox.layout.boxable; + +public interface WrappingFunction { + + String[] getLines(String text); +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Document.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Document.java index 292e099..d4bf333 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Document.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Document.java @@ -93,7 +93,7 @@ public class Document implements RenderListener { private Entry createEntry(final Element element, final LayoutHint layoutHint) { - return new SimpleEntry(element, layoutHint); + return new SimpleEntry<>(element, layoutHint); } /** @@ -237,31 +237,20 @@ public class Document implements RenderListener { Element element = entry.getKey(); LayoutHint layoutHint = entry.getValue(); boolean success = false; - - // first ask custom renderer to render the element - Iterator customRendererIterator = customRenderer - .iterator(); + Iterator customRendererIterator = customRenderer.iterator(); while (!success && customRendererIterator.hasNext()) { - success = customRendererIterator.next().render(renderContext, - element, layoutHint); + success = customRendererIterator.next().render(renderContext, element, layoutHint); } - - // if none of them felt responsible, let the default renderer do the job. if (!success) { - success = renderContext.render(renderContext, element, - layoutHint); + success = renderContext.render(renderContext, element, layoutHint); } - if (!success) { - throw new IllegalArgumentException( - String.format( + throw new IllegalArgumentException(String.format( "neither layout %s nor the render context knows what to do with %s", renderContext.getLayout(), element)); - } } renderContext.close(); - resetPDDocument(); return document; } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Frame.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Frame.java index db3e65e..920e6e7 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Frame.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Frame.java @@ -10,9 +10,9 @@ import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.WidthRespecting; import java.awt.Color; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; /** * The frame is a container for a {@link Drawable}, that allows to add margin, @@ -23,7 +23,7 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class Frame implements Element, Drawable, WidthRespecting, Dividable { - private final List innerList = new CopyOnWriteArrayList<>(); + private final List innerList = new ArrayList<>(); private float paddingLeft; private float paddingRight; diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Paragraph.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Paragraph.java index 9faeb59..57d472a 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Paragraph.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/Paragraph.java @@ -8,7 +8,6 @@ import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.TextFlow; import org.xbib.graphics.pdfbox.layout.text.TextSequenceUtil; import org.xbib.graphics.pdfbox.layout.text.WidthRespecting; -import java.io.IOException; /** * A paragraph is used as a container for {@link TextFlow text} that is drawn as @@ -54,23 +53,22 @@ public class Paragraph extends TextFlow implements Drawable, Element, WidthRespe @Override public void draw(PDDocument pdDocument, PDPageContentStream contentStream, - Position upperLeft, DrawListener drawListener) throws IOException { + Position upperLeft, DrawListener drawListener) { drawText(contentStream, upperLeft, getAlignment(), drawListener); } @Override - public Divided divide(float remainingHeight, final float pageHeight) - throws IOException { + public Divided divide(float remainingHeight, final float pageHeight) { return TextSequenceUtil.divide(this, getMaxWidth(), remainingHeight); } @Override - public Paragraph removeLeadingEmptyVerticalSpace() throws IOException { + public Paragraph removeLeadingEmptyVerticalSpace() { return removeLeadingEmptyLines(); } @Override - public Paragraph removeLeadingEmptyLines() throws IOException { + public Paragraph removeLeadingEmptyLines() { Paragraph result = (Paragraph) super.removeLeadingEmptyLines(); result.setAbsolutePosition(this.getAbsolutePosition()); result.setAlignment(this.getAlignment()); diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/RenderContext.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/RenderContext.java index 12a20a7..69146de 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/RenderContext.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/RenderContext.java @@ -4,6 +4,7 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.util.Matrix; import org.xbib.graphics.pdfbox.layout.elements.ControlElement; import org.xbib.graphics.pdfbox.layout.elements.Document; import org.xbib.graphics.pdfbox.layout.elements.Element; @@ -17,7 +18,6 @@ import org.xbib.graphics.pdfbox.layout.text.DrawContext; import org.xbib.graphics.pdfbox.layout.text.DrawListener; import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationDrawListener; -import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper; import java.io.Closeable; import java.io.IOException; @@ -204,8 +204,7 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList * @return true if the page is rotated by 90/270 degrees. */ public boolean isPageTilted() { - return CompatibilityHelper.getPageRotation(page) == 90 - || CompatibilityHelper.getPageRotation(page) == 270; + return page.getRotation() == 90 || page.getRotation() == 270; } /** @@ -374,12 +373,9 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList if (nextPageFormat != null) { setPageFormat(nextPageFormat); } - this.page = new PDPage(getPageFormat().getMediaBox()); this.pdDocument.addPage(page); - this.contentStream = CompatibilityHelper - .createAppendablePDPageContentStream(pdDocument, page); - + this.contentStream = new PDPageContentStream(pdDocument, page, PDPageContentStream.AppendMode.APPEND, true); // fix orientation if (getPageOrientation() != getPageFormat().getOrientation()) { if (isPageTilted()) { @@ -389,7 +385,7 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList } } if (isPageTilted()) { - CompatibilityHelper.transform(contentStream, 0, 1, -1, 0, getPageHeight(), 0); + contentStream.transform(new Matrix(0, 1, -1, 0, getPageHeight(), 0)); } resetPositionToUpperLeft(); resetMaxPositionOnPage(); @@ -410,8 +406,7 @@ public class RenderContext implements Renderer, Closeable, DrawContext, DrawList document.afterPage(this); if (getPageFormat().getRotation() != 0) { - int currentRotation = CompatibilityHelper - .getPageRotation(getCurrentPage()); + int currentRotation = getCurrentPage().getRotation(); getCurrentPage().setRotation( currentRotation + getPageFormat().getRotation()); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/VerticalLayout.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/VerticalLayout.java index 1173663..af2b79c 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/VerticalLayout.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/elements/render/VerticalLayout.java @@ -11,7 +11,6 @@ import org.xbib.graphics.pdfbox.layout.elements.VerticalSpacer; import org.xbib.graphics.pdfbox.layout.text.Alignment; import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.WidthRespecting; -import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper; import java.io.IOException; /** @@ -246,17 +245,13 @@ public class VerticalLayout implements Layout { break; } } - contentStream.saveGraphicsState(); contentStream.addRect(0, pageFormat.getMarginBottom(), renderContext.getPageWidth(), renderContext.getHeight()); - CompatibilityHelper.clip(contentStream); - + contentStream.clip(); drawable.draw(renderContext.getPdDocument(), contentStream, renderContext.getCurrentPosition().add(offsetX, 0), renderContext); - contentStream.restoreGraphicsState(); - if (movePosition) { renderContext.movePositionBy(0, -drawable.getHeight()); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/BaseFont.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/BaseFont.java index 1421733..1540c94 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/BaseFont.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/BaseFont.java @@ -12,10 +12,10 @@ import org.apache.pdfbox.pdmodel.font.PDType1Font; public enum BaseFont implements Font { TIMES(PDType1Font.TIMES_ROMAN, PDType1Font.TIMES_BOLD, - PDType1Font.TIMES_ITALIC, PDType1Font.TIMES_BOLD_ITALIC), // + PDType1Font.TIMES_ITALIC, PDType1Font.TIMES_BOLD_ITALIC), COURIER(PDType1Font.COURIER, PDType1Font.COURIER_BOLD, - PDType1Font.COURIER_OBLIQUE, PDType1Font.COURIER_BOLD_OBLIQUE), // + PDType1Font.COURIER_OBLIQUE, PDType1Font.COURIER_BOLD_OBLIQUE), HELVETICA(PDType1Font.HELVETICA, PDType1Font.HELVETICA_BOLD, PDType1Font.HELVETICA_OBLIQUE, PDType1Font.HELVETICA_BOLD_OBLIQUE); @@ -28,8 +28,7 @@ public enum BaseFont implements Font { private final PDFont boldItalicFont; - BaseFont(PDFont plainFont, PDFont boldFont, PDFont italicFont, - PDFont boldItalicFont) { + BaseFont(PDFont plainFont, PDFont boldFont, PDFont italicFont, PDFont boldItalicFont) { this.plainFont = plainFont; this.boldFont = boldFont; this.italicFont = italicFont; @@ -37,7 +36,7 @@ public enum BaseFont implements Font { } @Override - public PDFont getPlainFont() { + public PDFont getRegularFont() { return plainFont; } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/Font.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/Font.java index c86b5e0..3c90d67 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/Font.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/Font.java @@ -4,7 +4,7 @@ import org.apache.pdfbox.pdmodel.font.PDFont; public interface Font { - PDFont getPlainFont(); + PDFont getRegularFont(); PDFont getBoldFont(); diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/FontDescriptor.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/FontDescriptor.java index 04628f4..3494ac7 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/FontDescriptor.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/FontDescriptor.java @@ -1,47 +1,56 @@ package org.xbib.graphics.pdfbox.layout.font; import org.apache.pdfbox.pdmodel.font.PDFont; +import java.util.Objects; /** * Container for a Font and size. */ public class FontDescriptor { - /** - * the associated font. - */ - private final PDFont font; + private final Font font; - /** - * the font size. - */ private final float size; - /** - * Creates the descriptor the the given font and size. - * - * @param font the font. - * @param size the size. - */ - public FontDescriptor(final PDFont font, final float size) { - this.font = font; - this.size = size; + private final boolean regular; + + private final boolean bold; + + private final boolean italic; + + public FontDescriptor(Font font, float size) { + this(font, size, false, false); } - /** - * @return the font. - */ - public PDFont getFont() { + public FontDescriptor(Font font, float size, boolean bold, boolean italic) { + this.font = font; + this.size = size; + this.regular = !bold && !italic; + this.bold = bold; + this.italic = italic; + } + + public Font getFont() { return font; } - /** - * @return the size. - */ public float getSize() { return size; } + public PDFont getSelectedFont() { + if (regular) { + return font.getRegularFont(); + } + if (italic) { + return bold ? font.getBoldItalicFont() : font.getItalicFont(); + } + if (bold) { + return font.getBoldFont(); + } + throw new IllegalStateException(); + } + @Override public String toString() { return "FontDescriptor [font=" + font + ", size=" + size + "]"; @@ -49,33 +58,14 @@ public class FontDescriptor { @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((font == null) ? 0 : font.hashCode()); - result = prime * result + Float.floatToIntBits(size); - return result; + return Objects.hash(font, size, regular, bold, italic); } @Override public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { + if (!(obj instanceof FontDescriptor)) { return false; } - if (getClass() != obj.getClass()) { - return false; - } - 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); + return Objects.hashCode(obj) == Objects.hash(this); } - } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/NotoSansFont.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/NotoSansFont.java index a51a080..b0b7063 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/NotoSansFont.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/font/NotoSansFont.java @@ -4,10 +4,13 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType0Font; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.Objects; public class NotoSansFont implements Font { + private final PDDocument document; + private static PDType0Font regular; private static PDType0Font bold; @@ -16,32 +19,47 @@ public class NotoSansFont implements Font { private static PDType0Font bolditalic; - public NotoSansFont(PDDocument document) throws IOException { - if (regular == null) { - regular = PDType0Font.load(document, Objects.requireNonNull(getClass().getResourceAsStream("NotoSans-Regular.ttf"))); - bold = PDType0Font.load(document, Objects.requireNonNull(getClass().getResourceAsStream("NotoSans-Bold.ttf"))); - italic = PDType0Font.load(document, Objects.requireNonNull(getClass().getResourceAsStream("NotoSans-Italic.ttf"))); - bolditalic = PDType0Font.load(document, Objects.requireNonNull(getClass().getResourceAsStream("NotoSans-BoldItalic.ttf"))); - } + public NotoSansFont(PDDocument document) { + this.document = document; } @Override - public PDFont getPlainFont() { + public PDFont getRegularFont() { + if (regular == null) { + regular = load(document, "NotoSans-Regular.ttf"); + } return regular; } @Override public PDFont getBoldFont() { + if (bold == null) { + bold = load(document, "NotoSans-Bold.ttf"); + } return bold; } @Override public PDFont getItalicFont() { + if (italic == null) { + italic = load(document, "NotoSans-Italic.ttf"); + } return italic; } @Override public PDFont getBoldItalicFont() { + if (bolditalic == null) { + bolditalic = load(document, "NotoSans-BoldItalic.ttf"); + } return bolditalic; } + + private static PDType0Font load(PDDocument document, String resourceName) { + try { + return PDType0Font.load(document, Objects.requireNonNull(NotoSansFont.class.getResourceAsStream(resourceName))); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/AbstractShape.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/AbstractShape.java index fbd0211..86f7ad3 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/AbstractShape.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/AbstractShape.java @@ -4,16 +4,13 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.xbib.graphics.pdfbox.layout.text.DrawListener; import org.xbib.graphics.pdfbox.layout.text.Position; -import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper; import java.awt.Color; import java.io.IOException; /** * Abstract base class for shapes which performs the * {@link #fill(PDDocument, PDPageContentStream, Position, float, float, Color, DrawListener)} - * and (@link - * {@link #draw(PDDocument, PDPageContentStream, Position, float, float, Color, Stroke, DrawListener)} - * . + * and {@link #draw(PDDocument, PDPageContentStream, Position, float, float, Color, Stroke, DrawListener)} */ public abstract class AbstractShape implements Shape { @@ -21,9 +18,7 @@ public abstract class AbstractShape implements Shape { public void draw(PDDocument pdDocument, PDPageContentStream contentStream, Position upperLeft, float width, float height, Color color, Stroke stroke, DrawListener drawListener) throws IOException { - add(pdDocument, contentStream, upperLeft, width, height); - if (stroke != null) { stroke.applyTo(contentStream); } @@ -31,29 +26,22 @@ public abstract class AbstractShape implements Shape { contentStream.setStrokingColor(color); } contentStream.stroke(); - if (drawListener != null) { drawListener.drawn(this, upperLeft, width, height); } - } @Override public void fill(PDDocument pdDocument, PDPageContentStream contentStream, Position upperLeft, float width, float height, Color color, DrawListener drawListener) throws IOException { - add(pdDocument, contentStream, upperLeft, width, height); - if (color != null) { contentStream.setNonStrokingColor(color); } - CompatibilityHelper.fillNonZero(contentStream); - + contentStream.fill(); if (drawListener != null) { drawListener.drawn(this, upperLeft, width, height); } - } - } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/RoundRect.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/RoundRect.java index 4e616b0..4f60938 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/RoundRect.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/RoundRect.java @@ -3,7 +3,6 @@ package org.xbib.graphics.pdfbox.layout.shape; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.xbib.graphics.pdfbox.layout.text.Position; -import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper; import java.io.IOException; /** @@ -14,6 +13,7 @@ public class RoundRect extends AbstractShape { private final static float BEZ = 0.551915024494f; private final float cornerRadiusX; + private final float cornerRadiusY; /** @@ -96,18 +96,16 @@ public class RoundRect extends AbstractShape { contentStream.moveTo(a.getX(), a.getY()); addLine(contentStream, a.getX(), a.getY(), b.getX(), b.getY()); - CompatibilityHelper.curveTo(contentStream, b.getX() + bezX, b.getY(), c.getX(), + contentStream.curveTo(b.getX() + bezX, b.getY(), c.getX(), c.getY() + bezY, c.getX(), c.getY()); - // contentStream.addLine(c.getX(), c.getY(), d.getX(), d.getY()); addLine(contentStream, c.getX(), c.getY(), d.getX(), d.getY()); - CompatibilityHelper.curveTo(contentStream, d.getX(), d.getY() - bezY, e.getX() + bezX, + contentStream.curveTo(d.getX(), d.getY() - bezY, e.getX() + bezX, e.getY(), e.getX(), e.getY()); - // contentStream.addLine(e.getX(), e.getY(), f.getX(), f.getY()); addLine(contentStream, e.getX(), e.getY(), f.getX(), f.getY()); - CompatibilityHelper.curveTo(contentStream, f.getX() - bezX, f.getY(), g.getX(), + contentStream.curveTo(f.getX() - bezX, f.getY(), g.getX(), g.getY() - bezY, g.getX(), g.getY()); addLine(contentStream, g.getX(), g.getY(), h.getX(), h.getY()); - CompatibilityHelper.curveTo(contentStream, h.getX(), h.getY() + bezY, a.getX() - bezX, + contentStream.curveTo(h.getX(), h.getY() + bezY, a.getX() - bezX, a.getY(), a.getX(), a.getY()); } @@ -120,7 +118,7 @@ public class RoundRect extends AbstractShape { float y1, float x2, float y2) throws IOException { float xMid = (x1 + x2) / 2f; float yMid = (y1 + y2) / 2f; - CompatibilityHelper.curveTo1(contentStream, xMid, yMid, x2, y2); + contentStream.curveTo1(xMid, yMid, x2, y2); } } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/Stroke.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/Stroke.java index c5bc91e..b90c899 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/Stroke.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/shape/Stroke.java @@ -2,6 +2,8 @@ package org.xbib.graphics.pdfbox.layout.shape; import org.apache.pdfbox.pdmodel.PDPageContentStream; import java.io.IOException; +import java.util.Arrays; +import java.util.logging.Logger; /** * This is a container for all information needed to perform a stroke. @@ -51,6 +53,7 @@ public class Stroke { public static class DashPattern { private final float[] pattern; + private final float phase; /** @@ -106,8 +109,11 @@ public class Stroke { } private final CapStyle capStyle; + private final JoinStyle joinStyle; + private final DashPattern dashPattern; + private final float lineWidth; /** @@ -168,18 +174,20 @@ public class Stroke { * @param contentStream the content stream to apply this stroke to. * @throws IOException by PDFBox. */ - public void applyTo(final PDPageContentStream contentStream) - throws IOException { + public void applyTo(PDPageContentStream contentStream) throws IOException { if (getCapStyle() != null) { + Logger.getLogger("").info(" cap style = " + getCapStyle().value()); contentStream.setLineCapStyle(getCapStyle().value()); } if (getJoinStyle() != null) { + Logger.getLogger("").info(" join style = " + getJoinStyle().value()); contentStream.setLineJoinStyle(getJoinStyle().value()); } if (getDashPattern() != null) { - contentStream.setLineDashPattern(getDashPattern().getPattern(), - getDashPattern().getPhase()); + Logger.getLogger("").info(" dash pattern = " + Arrays.asList(getDashPattern().getPattern())); + contentStream.setLineDashPattern(getDashPattern().getPattern(), getDashPattern().getPhase()); } + Logger.getLogger("").info(" line width = " + getLineWidth()); contentStream.setLineWidth(getLineWidth()); } @@ -196,10 +204,14 @@ public class Stroke { * A builder providing a fluent interface for creating a stroke. */ public static class StrokeBuilder { + private CapStyle capStyle = CapStyle.Cap; + private JoinStyle joinStyle = JoinStyle.Miter; + private DashPattern dashPattern; - float lineWidth = 1f; + + private float lineWidth = 1f; public StrokeBuilder capStyle(CapStyle capStyle) { this.capStyle = capStyle; diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/AbstractCell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/AbstractCell.java new file mode 100644 index 0000000..e797803 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/AbstractCell.java @@ -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 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 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> { + + 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; + } + }*/ +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/AbstractTextCell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/AbstractTextCell.java new file mode 100644 index 0000000..3954231 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/AbstractTextCell.java @@ -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 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> extends AbstractCellBuilder { + + 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; + } + + }*/ +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/BorderStyle.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/BorderStyle.java new file mode 100644 index 0000000..d4bb92d --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/BorderStyle.java @@ -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; + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/BorderStyleInterface.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/BorderStyleInterface.java new file mode 100644 index 0000000..30570b3 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/BorderStyleInterface.java @@ -0,0 +1,10 @@ +package org.xbib.graphics.pdfbox.layout.table; + +public interface BorderStyleInterface { + + float[] getPattern(); + + int getPhase(); + +} + diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Column.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Column.java new file mode 100644 index 0000000..0eaa082 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Column.java @@ -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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/CouldNotDetermineStringWidthException.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/CouldNotDetermineStringWidthException.java new file mode 100644 index 0000000..c46dbe8 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/CouldNotDetermineStringWidthException.java @@ -0,0 +1,9 @@ +package org.xbib.graphics.pdfbox.layout.table; + +@SuppressWarnings("serial") +public class CouldNotDetermineStringWidthException extends RuntimeException { + + public CouldNotDetermineStringWidthException() { + super(); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/HorizontalAlignment.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/HorizontalAlignment.java new file mode 100644 index 0000000..e8eb08a --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/HorizontalAlignment.java @@ -0,0 +1,7 @@ +package org.xbib.graphics.pdfbox.layout.table; + +public enum HorizontalAlignment { + + LEFT, CENTER, RIGHT, JUSTIFY + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Hyperlink.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Hyperlink.java new file mode 100644 index 0000000..b1e2fec --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Hyperlink.java @@ -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))); + } + +} \ No newline at end of file diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ImageCell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ImageCell.java new file mode 100644 index 0000000..3f5ba86 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ImageCell.java @@ -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; + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Markup.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Markup.java new file mode 100644 index 0000000..abcd903 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Markup.java @@ -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 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())); + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/NewLine.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/NewLine.java new file mode 100644 index 0000000..b0898e3 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/NewLine.java @@ -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))); + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ParagraphCell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ParagraphCell.java new file mode 100644 index 0000000..4bad104 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ParagraphCell.java @@ -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 processables; + + private final org.xbib.graphics.pdfbox.layout.elements.Paragraph wrappedParagraph = + new org.xbib.graphics.pdfbox.layout.elements.Paragraph(); + + public Paragraph(List processables) { + this.processables = processables; + } + + public List getProcessables() { + return processables; + } + + public org.xbib.graphics.pdfbox.layout.elements.Paragraph getWrappedParagraph() { + return wrappedParagraph; + } + + /*public static class ParagraphBuilder { + + private final List 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> extends AbstractCellBuilder { + + 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; + } + }*/ +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ParagraphProcessor.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ParagraphProcessor.java new file mode 100644 index 0000000..81d0118 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/ParagraphProcessor.java @@ -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; + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/PdfUtil.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/PdfUtil.java new file mode 100644 index 0000000..bdc8b4a --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/PdfUtil.java @@ -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 codePointsAsString = text.codePoints() + .mapToObj(codePoint -> new String(new int[]{codePoint}, 0, 1)) + .collect(Collectors.toList()); + + List 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 getOptimalTextBreakLines(final String text, final Font font, final int fontSize, final float maxWidth) { + List 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 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 goodLines = new ArrayList<>(); + Stack 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 splitBySize(final String line, final Font font, final int fontSize, final float maxWidth) { + List 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 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 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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/RepeatedHeaderTableRenderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/RepeatedHeaderTableRenderer.java new file mode 100644 index 0000000..9cd88be --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/RepeatedHeaderTableRenderer.java @@ -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 documentSupplier, Supplier 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; + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Row.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Row.java new file mode 100644 index 0000000..73c5835 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Row.java @@ -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 cells; + + private Settings settings; + + private Float height; + + private Row next; + + private Row(final List 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 getCells() { + return cells; + } + + public Table getTable() { + return table; + } + + public void setCells(List 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 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; + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/RowIsTooHighException.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/RowIsTooHighException.java new file mode 100644 index 0000000..1f013c2 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/RowIsTooHighException.java @@ -0,0 +1,10 @@ +package org.xbib.graphics.pdfbox.layout.table; + +@SuppressWarnings("serial") +public class RowIsTooHighException extends RuntimeException { + + public RowIsTooHighException(String message) { + super(message); + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Settings.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Settings.java new file mode 100644 index 0000000..d1b48c7 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Settings.java @@ -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(); + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/StyledText.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/StyledText.java new file mode 100644 index 0000000..404eec8 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/StyledText.java @@ -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))); + } + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Table.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Table.java new file mode 100644 index 0000000..a5c3f34 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/Table.java @@ -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 rows; + + private final List columns; + + private final Set rowSpanCells; + + private Settings settings; + + private int numberOfColumns; + + private float width; + + public Table(List rows, List columns, Set 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 getColumns() { + return columns; + } + + public List getRows() { + return rows; + } + + public static Font getDefaultFont() { + return DEFAULT_FONT; + } + + public Set 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 rows = new ArrayList<>(); + + private final List columns = new ArrayList<>(); + + private final Set 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 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 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(); + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableNotYetBuiltException.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableNotYetBuiltException.java new file mode 100644 index 0000000..e206d46 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableNotYetBuiltException.java @@ -0,0 +1,5 @@ +package org.xbib.graphics.pdfbox.layout.table; + +@SuppressWarnings("serial") +public class TableNotYetBuiltException extends RuntimeException { +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableRenderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableRenderer.java new file mode 100644 index 0000000..b6f055a --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableRenderer.java @@ -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> 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 computeRowsOnPagesWithNewPageStartOf(float yOffsetOnNewPage) { + final Queue 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 documentSupplier, Supplier pageSupplier, float yOffset) throws IOException { + PDDocument document = documentSupplier.get(); + float startOnNewPage = pageSupplier.get().getMediaBox().getHeight() - yOffset; + determinePageToStartTable(startOnNewPage); + final Queue 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 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 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 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; + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableSetupException.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableSetupException.java new file mode 100644 index 0000000..d5bed6d --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TableSetupException.java @@ -0,0 +1,9 @@ +package org.xbib.graphics.pdfbox.layout.table; + +@SuppressWarnings("serial") +public class TableSetupException extends RuntimeException { + + public TableSetupException(String message) { + super(message); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TextCell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TextCell.java new file mode 100644 index 0000000..1129028 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/TextCell.java @@ -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; + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/VerticalAlignment.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/VerticalAlignment.java new file mode 100644 index 0000000..96d3004 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/VerticalAlignment.java @@ -0,0 +1,7 @@ +package org.xbib.graphics.pdfbox.layout.table; + +public enum VerticalAlignment { + + BOTTOM, MIDDLE, TOP + +} \ No newline at end of file diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/VerticalTextCell.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/VerticalTextCell.java new file mode 100644 index 0000000..13346f8 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/VerticalTextCell.java @@ -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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/AbstractCellRenderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/AbstractCellRenderer.java new file mode 100644 index 0000000..7e3933e --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/AbstractCellRenderer.java @@ -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 implements Renderer { + + protected T cell; + + public AbstractCellRenderer 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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/ImageCellRenderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/ImageCellRenderer.java new file mode 100644 index 0000000..795c9d4 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/ImageCellRenderer.java @@ -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 { + + 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(); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PageNotSetException.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PageNotSetException.java new file mode 100644 index 0000000..d44246a --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PageNotSetException.java @@ -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); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/ParagraphCellRenderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/ParagraphCellRenderer.java new file mode 100644 index 0000000..dca04bd --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/ParagraphCellRenderer.java @@ -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 { + + private static final Map 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(); + } + }); + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedLine.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedLine.java new file mode 100644 index 0000000..567cc43 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedLine.java @@ -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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedRectangle.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedRectangle.java new file mode 100644 index 0000000..db5ed38 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedRectangle.java @@ -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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedStyledText.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedStyledText.java new file mode 100644 index 0000000..275c52a --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/PositionedStyledText.java @@ -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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/RenderContext.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/RenderContext.java new file mode 100644 index 0000000..335fcb7 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/RenderContext.java @@ -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; + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/RenderUtil.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/RenderUtil.java new file mode 100644 index 0000000..9fa7e92 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/RenderUtil.java @@ -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); + } + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/Renderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/Renderer.java new file mode 100644 index 0000000..59937a8 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/Renderer.java @@ -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); + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/TextCellRenderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/TextCellRenderer.java new file mode 100644 index 0000000..fa78073 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/TextCellRenderer.java @@ -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 extends AbstractCellRenderer { + + 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 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 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 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); + } +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/VerticalTextCellRenderer.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/VerticalTextCellRenderer.java new file mode 100644 index 0000000..c197c87 --- /dev/null +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/table/render/VerticalTextCellRenderer.java @@ -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 { + + 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 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); + } + } + +} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/Area.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/Area.java index eb25103..4c216bf 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/Area.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/Area.java @@ -1,7 +1,5 @@ package org.xbib.graphics.pdfbox.layout.text; -import java.io.IOException; - /** * Defines an area with a width and height. */ @@ -9,13 +7,11 @@ public interface Area { /** * @return the width of the area. - * @throws IOException by pdfbox */ - float getWidth() throws IOException; + float getWidth(); /** * @return the height of the area. - * @throws IOException by pdfbox */ - float getHeight() throws IOException; + float getHeight(); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/ControlFragment.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/ControlFragment.java index d9f19ab..195a1c6 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/ControlFragment.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/ControlFragment.java @@ -1,9 +1,8 @@ package org.xbib.graphics.pdfbox.layout.text; -import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.xbib.graphics.pdfbox.layout.font.BaseFont; import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import java.awt.Color; -import java.io.IOException; /** * A control fragment has no drawable representation but is meant to control the @@ -11,21 +10,23 @@ import java.io.IOException; */ public class ControlFragment implements TextFragment { - protected final static FontDescriptor DEFAULT_FONT_DESCRIPTOR = new FontDescriptor( - PDType1Font.HELVETICA, 11); + protected final static FontDescriptor DEFAULT_FONT_DESCRIPTOR = + new FontDescriptor(BaseFont.HELVETICA, 11); private String name; + private final String text; + private final FontDescriptor fontDescriptor; + private final Color color; - protected ControlFragment(final String text, - final FontDescriptor fontDescriptor) { + protected ControlFragment(String text, FontDescriptor fontDescriptor) { this(null, text, fontDescriptor, Color.black); } - protected ControlFragment(final String name, final String text, - final FontDescriptor fontDescriptor, final Color color) { + protected ControlFragment(String name, final String text, + FontDescriptor fontDescriptor, final Color color) { this.name = name; if (this.name == null) { this.name = getClass().getSimpleName(); @@ -36,12 +37,12 @@ public class ControlFragment implements TextFragment { } @Override - public float getWidth() throws IOException { + public float getWidth() { return 0; } @Override - public float getHeight() throws IOException { + public float getHeight() { return getFontDescriptor() == null ? 0 : getFontDescriptor().getSize(); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/Indent.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/Indent.java index 515a24c..0ca3d9e 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/Indent.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/Indent.java @@ -1,9 +1,9 @@ package org.xbib.graphics.pdfbox.layout.text; -import org.apache.pdfbox.pdmodel.font.PDFont; import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import java.awt.Color; import java.io.IOException; +import java.io.UncheckedIOException; /** * Control fragment that represents a indent in text. @@ -24,12 +24,9 @@ public class Indent extends ControlFragment { * * @param indentWidth the indentation. * @param indentUnit the indentation unit. - * @throws IOException by pdfbox */ - public Indent(final float indentWidth, final SpaceUnit indentUnit) - throws IOException { - this("", indentWidth, indentUnit, DEFAULT_FONT_DESCRIPTOR, - Alignment.LEFT, Color.black); + public Indent(float indentWidth, SpaceUnit indentUnit) { + this("", indentWidth, indentUnit, DEFAULT_FONT_DESCRIPTOR, Alignment.LEFT, Color.black); } /** @@ -40,16 +37,10 @@ public class Indent extends ControlFragment { * @param label the label of the indentation. * @param indentWidth the indentation. * @param indentUnit the indentation unit. - * @param fontSize the font size, resp. the height of the new line. - * @param font the font to use. - * @throws IOException by pdfbox + * @param descriptor the font size, resp. the height of the new line, the font to use. */ - public Indent(final String label, final float indentWidth, - final SpaceUnit indentUnit, final float fontSize, final PDFont font) - throws IOException { - - this(label, indentWidth, indentUnit, fontSize, font, Alignment.LEFT, - Color.black); + public Indent(String label, float indentWidth, SpaceUnit indentUnit, FontDescriptor descriptor) { + this(label, indentWidth, indentUnit, descriptor, Alignment.LEFT, Color.black); } /** @@ -60,38 +51,15 @@ public class Indent extends ControlFragment { * @param label the label of the indentation. * @param indentWidth the indentation. * @param indentUnit the indentation unit. - * @param fontSize the font size, resp. the height of the new line. - * @param font the font to use. + * @param fontDescriptor the font size, resp. the height of the new line, the font to use. * @param alignment the alignment of the label. - * @throws IOException by pdfbox */ - public Indent(final String label, final float indentWidth, - final SpaceUnit indentUnit, final float fontSize, - final PDFont font, final Alignment alignment) throws IOException { - this(label, indentWidth, indentUnit, fontSize, font, alignment, - Color.black); - } - - /** - * Creates a new line with the - * {@link ControlFragment#DEFAULT_FONT_DESCRIPTOR}'s font and the given - * height. - * - * @param label the label of the indentation. - * @param indentWidth the indentation. - * @param indentUnit the indentation unit. - * @param fontSize the font size, resp. the height of the new line. - * @param font the font to use. - * @param alignment the alignment of the label. - * @param color the color to use. - * @throws IOException by pdfbox - */ - public Indent(final String label, final float indentWidth, - final SpaceUnit indentUnit, final float fontSize, - final PDFont font, final Alignment alignment, final Color color) - throws IOException { - this(label, indentWidth, indentUnit, - new FontDescriptor(font, fontSize), alignment, color); + public Indent(String label, + float indentWidth, + SpaceUnit indentUnit, + FontDescriptor fontDescriptor, + Alignment alignment) { + this(label, indentWidth, indentUnit, fontDescriptor, alignment, Color.black); } /** @@ -103,37 +71,40 @@ public class Indent extends ControlFragment { * @param fontDescriptor the font and size associated with this new line. * @param alignment the alignment of the label. * @param color the color to use. - * @throws IOException by pdfbox */ - public Indent(final String label, final float indentWidth, - final SpaceUnit indentUnit, final FontDescriptor fontDescriptor, - final Alignment alignment, final Color color) throws IOException { + public Indent(String label, + float indentWidth, + SpaceUnit indentUnit, + FontDescriptor fontDescriptor, + Alignment alignment, + Color color) { super("INDENT", label, fontDescriptor, color); - - float indent = calculateIndent(indentWidth, indentUnit, fontDescriptor); - float textWidth = 0; - if (label != null && !label.isEmpty()) { - textWidth = fontDescriptor.getSize() - * fontDescriptor.getFont().getStringWidth(label) / 1000f; - } - float marginLeft = 0; - float marginRight = 0; - if (textWidth < indent) { - switch (alignment) { - case LEFT: - marginRight = indent - textWidth; - break; - case RIGHT: - marginLeft = indent - textWidth; - break; - default: - marginLeft = (indent - textWidth) / 2f; - marginRight = marginLeft; - break; + try { + float indent = calculateIndent(indentWidth, indentUnit, fontDescriptor); + float textWidth = 0; + if (label != null && !label.isEmpty()) { + textWidth = fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getStringWidth(label) / 1000f; } + float marginLeft = 0; + float marginRight = 0; + if (textWidth < indent) { + switch (alignment) { + case LEFT: + marginRight = indent - textWidth; + break; + case RIGHT: + marginLeft = indent - textWidth; + break; + default: + marginLeft = (indent - textWidth) / 2f; + marginRight = marginLeft; + break; + } + } + styledText = new StyledText(label, fontDescriptor, getColor(), 0, marginLeft, marginRight); + } catch (IOException e) { + throw new UncheckedIOException(e); } - styledText = new StyledText(label, getFontDescriptor(), getColor(), 0, - marginLeft, marginRight); } /** @@ -147,8 +118,7 @@ public class Indent extends ControlFragment { indentPt, 0); } - private float calculateIndent(final float indentWidth, - final SpaceUnit indentUnit, final FontDescriptor fontDescriptor) + private float calculateIndent(float indentWidth, SpaceUnit indentUnit, final FontDescriptor fontDescriptor) throws IOException { if (indentWidth < 0) { return 0; @@ -157,13 +127,10 @@ public class Indent extends ControlFragment { } @Override - public float getWidth() throws IOException { + public float getWidth() { return styledText.getWidth(); } - /** - * @return a styled text representation of the indent. - */ public StyledText toStyledText() { return styledText; } @@ -172,5 +139,4 @@ public class Indent extends ControlFragment { public String toString() { return "ControlFragment [" + getName() + ", " + styledText + "]"; } - } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/IndentCharacters.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/IndentCharacters.java index 5955d69..d1ff110 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/IndentCharacters.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/IndentCharacters.java @@ -1,11 +1,9 @@ package org.xbib.graphics.pdfbox.layout.text; -import org.apache.pdfbox.pdmodel.font.PDFont; -import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper; +import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import org.xbib.graphics.pdfbox.layout.util.Enumerator; import org.xbib.graphics.pdfbox.layout.util.EnumeratorFactory; import java.awt.Color; -import java.io.IOException; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -23,8 +21,7 @@ public class IndentCharacters { /** * Represent un-indentation, means effectively indent of 0. */ - public static IndentCharacter UNINDENT_CHARACTER = new IndentCharacter("0", - "0", "pt"); + public static IndentCharacter UNINDENT_CHARACTER = new IndentCharacter("0", "0", "pt"); /** * An --{7em} indicates an indentation of 7 characters in markup, @@ -38,8 +35,9 @@ public class IndentCharacters { protected float indentWidth = 4; protected SpaceUnit indentUnit = SpaceUnit.em; - public IndentCharacter(final String level, final String indentWidth, - final String indentUnit) { + public IndentCharacter(String level, + String indentWidth, + String indentUnit) { super("INDENT", IndentCharacterFactory.TO_ESCAPE); try { this.level = level == null ? 0 : level.length() + 1; @@ -78,16 +76,12 @@ public class IndentCharacters { * Creates the actual {@link Indent} fragment from this control * character. * - * @param fontSize the current font size. - * @param font the current font. + * @param descriptor the current font size, the current font. * @param color the color to use. * @return the new Indent. - * @throws IOException by pdfbox */ - public Indent createNewIndent(final float fontSize, final PDFont font, - final Color color) throws IOException { - return new Indent(nextLabel(), level * indentWidth, indentUnit, - fontSize, font, Alignment.RIGHT, color); + public Indent createNewIndent(FontDescriptor descriptor, Color color) { + return new Indent(nextLabel(), level * indentWidth, indentUnit, descriptor, Alignment.RIGHT, color); } @Override @@ -130,7 +124,7 @@ public class IndentCharacters { * markup, using -- as the bullet. The number, the unit, bullet * character and the brackets are optional. Default indentation is 4 * characters, default unit is em and the default bullet - * depends on {@link CompatibilityHelper#getBulletCharacter(int)}. It can be + * depends on {@link #getBulletCharacter(int)}. It can be * escaped with a backslash ('\'). */ public static class ListCharacter extends IndentCharacter { @@ -146,7 +140,7 @@ public class IndentCharacters { label += " "; } } else { - label = CompatibilityHelper.getBulletCharacter(getLevel()) + " "; + label = getBulletCharacter(getLevel()) + " "; } } @@ -316,6 +310,17 @@ public class IndentCharacters { public boolean patternMatchesBeginOfLine() { return true; } - } + + private static String getBulletCharacter(final int level) { + if (level % 2 == 1) { + return System.getProperty("pdfbox.layout.bullet.odd", BULLET); + } + return System.getProperty("pdfbox.layout.bullet.even", DOUBLE_ANGLE); + } + + private static final String BULLET = "\u2022"; + + private static final String DOUBLE_ANGLE = "\u00bb"; + } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/NewLine.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/NewLine.java index a868157..296d249 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/NewLine.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/NewLine.java @@ -8,32 +8,12 @@ import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; */ public class NewLine extends ControlFragment { - /** - * Creates a new line with the - * {@link ControlFragment#DEFAULT_FONT_DESCRIPTOR}. - */ - public NewLine() { - this(DEFAULT_FONT_DESCRIPTOR); - } - - /** - * Creates a new line with the - * {@link ControlFragment#DEFAULT_FONT_DESCRIPTOR}'s font and the given - * height. - * - * @param fontSize the font size, resp. the height of the new line. - */ - public NewLine(final float fontSize) { - this(new FontDescriptor(DEFAULT_FONT_DESCRIPTOR.getFont(), fontSize)); - } - /** * Creates a new line with the given font descriptor. * * @param fontDescriptor the font and size associated with this new line. */ - public NewLine(final FontDescriptor fontDescriptor) { + public NewLine(FontDescriptor fontDescriptor) { super("\n", fontDescriptor); } - } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/SpaceUnit.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/SpaceUnit.java index 9947c16..96deae7 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/SpaceUnit.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/SpaceUnit.java @@ -25,10 +25,9 @@ public enum SpaceUnit { * @return the size in pt. * @throws IOException by pdfbox */ - public float toPt(final float size, final FontDescriptor fontDescriptor) throws IOException { + public float toPt(float size, FontDescriptor fontDescriptor) throws IOException { if (this == em) { - return fontDescriptor.getSize() - * fontDescriptor.getFont().getAverageFontWidth() / 1000 * size; + return fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getAverageFontWidth() / 1000 * size; } return size; } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/StyledText.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/StyledText.java index f543678..2d7c60b 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/StyledText.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/StyledText.java @@ -1,9 +1,9 @@ package org.xbib.graphics.pdfbox.layout.text; -import org.apache.pdfbox.pdmodel.font.PDFont; import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import java.awt.Color; import java.io.IOException; +import java.io.UncheckedIOException; /** * Base class representing drawable text styled with font, size, color etc. @@ -27,51 +27,13 @@ public class StyledText implements TextFragment { */ private Float width = null; - /** - * Creates a styled text. - * - * @param text the text to draw. Must not contain line feeds ('\n'). - * @param size the size of the font. - * @param font the font to use. - */ - public StyledText(final String text, final float size, final PDFont font) { - this(text, size, font, Color.black); - } - - /** - * Creates a styled text. - * - * @param text the text to draw. Must not contain line feeds ('\n'). - * @param size the size of the font. - * @param font the font to use. - * @param color the color to use. - */ - public StyledText(final String text, final float size, final PDFont font, - final Color color) { - this(text, new FontDescriptor(font, size), color); - } - - /** - * Creates a styled text. - * - * @param text the text to draw. Must not contain line feeds ('\n'). - * @param size the size of the font. - * @param font the font to use. - * @param color the color to use. - * @param baselineOffset the offset of the baseline. - */ - public StyledText(final String text, final float size, final PDFont font, - final Color color, final float baselineOffset) { - this(text, new FontDescriptor(font, size), color, baselineOffset, 0, 0); - } - /** * Creates a styled text. * * @param text the text to draw. Must not contain line feeds ('\n'). * @param fontDescriptor the font to use. */ - public StyledText(final String text, final FontDescriptor fontDescriptor) { + public StyledText(String text, FontDescriptor fontDescriptor) { this(text, fontDescriptor, Color.black); } @@ -82,8 +44,7 @@ public class StyledText implements TextFragment { * @param fontDescriptor the font to use. * @param color the color to use. */ - public StyledText(final String text, final FontDescriptor fontDescriptor, - final Color color) { + public StyledText(String text, FontDescriptor fontDescriptor, Color color) { this(text, fontDescriptor, color, 0, 0, 0); } @@ -97,12 +58,12 @@ public class StyledText implements TextFragment { * @param leftMargin the margin left to the text. * @param rightMargin the margin right to the text. */ - public StyledText(final String text, final FontDescriptor fontDescriptor, - final Color color, final float baselineOffset, - final float leftMargin, final float rightMargin) { + public StyledText(String text, + FontDescriptor fontDescriptor, + Color color, + float baselineOffset, float leftMargin, float rightMargin) { if (text.contains("\n")) { - throw new IllegalArgumentException( - "StyledText must not contain line breaks, use TextFragment.LINEBREAK for that"); + throw new IllegalArgumentException("StyledText must not contain line breaks, use TextFragment.LINEBREAK for that"); } if (leftMargin < 0) { throw new IllegalArgumentException("leftMargin must be >= 0"); @@ -133,11 +94,9 @@ public class StyledText implements TextFragment { } @Override - public float getWidth() throws IOException { + public float getWidth() { if (width == null) { - width = getFontDescriptor().getSize() - * getFontDescriptor().getFont().getStringWidth(getText()) - / 1000; + width = getWidth(getFontDescriptor(), getText()); width += leftMargin; width += rightMargin; } @@ -149,7 +108,7 @@ public class StyledText implements TextFragment { } @Override - public float getHeight() throws IOException { + public float getHeight() { return getFontDescriptor().getSize(); } @@ -158,9 +117,7 @@ public class StyledText implements TextFragment { * @throws IOException by pdfbox. */ public float getAsent() throws IOException { - return getFontDescriptor().getSize() - * getFontDescriptor().getFont().getFontDescriptor().getAscent() - / 1000; + return getFontDescriptor().getSize() * getFontDescriptor().getSelectedFont().getFontDescriptor().getAscent() / 1000; } public float getBaselineOffset() { @@ -206,12 +163,19 @@ public class StyledText implements TextFragment { return inheritAttributes(text, getLeftMargin(), getRightMargin()); } - public StyledText inheritAttributes(String text, float leftMargin, - float rightMargin) { + public StyledText inheritAttributes(String text, float leftMargin, float rightMargin) { return new StyledText(text, getFontDescriptor(), getColor(), getBaselineOffset(), leftMargin, rightMargin); } + private static float getWidth(FontDescriptor fontDescriptor, String text) { + try { + return fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getStringWidth(text) / 1000; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + @Override public String toString() { return "StyledText [text=" + text + ", fontDescriptor=" @@ -219,5 +183,4 @@ public class StyledText implements TextFragment { + ", leftMargin=" + leftMargin + ", rightMargin=" + rightMargin + ", baselineOffset=" + baselineOffset + "]"; } - } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextFlow.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextFlow.java index c4e5372..3f628cc 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextFlow.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextFlow.java @@ -2,7 +2,7 @@ package org.xbib.graphics.pdfbox.layout.text; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.xbib.graphics.pdfbox.layout.font.Font; -import java.io.IOException; +import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; @@ -68,9 +68,8 @@ public class TextFlow implements TextSequence, WidthRespecting { return (T) cache.get(key); } - public void addText(final String text, final float fontSize, - final Font font) throws IOException { - add(TextFlowUtil.createTextFlow(text, fontSize, font)); + public void addText(String text, float fontSize, Font font) { + add(TextFlowUtil.createTextFlow(text, new FontDescriptor(font, fontSize))); } /** @@ -78,13 +77,20 @@ public class TextFlow implements TextSequence, WidthRespecting { * * @param markup the markup to add. * @param fontSize the font size to use. - * @param baseFont the base font describing the bundle of - * plain/blold/italic/bold-italic fonts. - * @throws IOException by PDFBox + * @param font the font */ - public void addMarkup(final String markup, final float fontSize, - final Font baseFont) throws IOException { - add(TextFlowUtil.createTextFlowFromMarkup(markup, fontSize, baseFont)); + public void addMarkup(String markup, float fontSize, Font font) { + add(TextFlowUtil.createTextFlowFromMarkup(markup, new FontDescriptor(font, fontSize))); + } + + public void addIndent(String label, float indentWidth, SpaceUnit indentUnit, + float fontsize, Font font) { + add(new Indent(label, indentWidth, indentUnit, new FontDescriptor(font, fontsize))); + } + + public void addIndent(String label, float indentWidth, SpaceUnit indentUnit, + float fontsize, Font font, Alignment alignment) { + add(new Indent(label, indentWidth, indentUnit, new FontDescriptor(font, fontsize), alignment)); } /** @@ -92,7 +98,7 @@ public class TextFlow implements TextSequence, WidthRespecting { * * @param sequence the sequence to add. */ - public void add(final TextSequence sequence) { + public void add(TextSequence sequence) { for (TextFragment fragment : sequence) { add(fragment); } @@ -103,7 +109,7 @@ public class TextFlow implements TextSequence, WidthRespecting { * * @param fragment the fragment to add. */ - public void add(final TextFragment fragment) { + public void add(TextFragment fragment) { text.add(fragment); clearCache(); } @@ -199,7 +205,7 @@ public class TextFlow implements TextSequence, WidthRespecting { } @Override - public float getWidth() throws IOException { + public float getWidth() { Float width = getCachedValue(WIDTH, Float.class); if (width == null) { width = TextSequenceUtil.getWidth(this, getMaxWidth()); @@ -209,7 +215,7 @@ public class TextFlow implements TextSequence, WidthRespecting { } @Override - public float getHeight() throws IOException { + public float getHeight() { Float height = getCachedValue(HEIGHT, Float.class); if (height == null) { height = TextSequenceUtil.getHeight(this, getMaxWidth(), @@ -221,23 +227,22 @@ public class TextFlow implements TextSequence, WidthRespecting { @Override public void drawText(PDPageContentStream contentStream, Position upperLeft, - Alignment alignment, DrawListener drawListener) throws IOException { + Alignment alignment, DrawListener drawListener) { TextSequenceUtil.drawText(this, contentStream, upperLeft, drawListener, alignment, getMaxWidth(), getLineSpacing(), isApplyLineSpacingToFirstLine()); } public void drawTextRightAligned(PDPageContentStream contentStream, - Position endOfFirstLine, DrawListener drawListener) throws IOException { + Position endOfFirstLine, DrawListener drawListener) { drawText(contentStream, endOfFirstLine.add(-getWidth(), 0), Alignment.RIGHT, drawListener); } /** * @return a copy of this text flow where all leading {@link NewLine}s are removed. - * @throws IOException by pdfbox. */ - public TextFlow removeLeadingEmptyLines() throws IOException { + public TextFlow removeLeadingEmptyLines() { if (text.size() == 0 || !(text.get(0) instanceof NewLine)) { return this; } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextFlowUtil.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextFlowUtil.java index 167cd4d..44b1cd3 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextFlowUtil.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextFlowUtil.java @@ -1,12 +1,10 @@ package org.xbib.graphics.pdfbox.layout.text; -import org.apache.pdfbox.pdmodel.font.PDFont; -import org.xbib.graphics.pdfbox.layout.font.Font; +import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotatedStyledText; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotation; import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationCharacters; import java.awt.Color; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -17,11 +15,9 @@ import java.util.regex.Matcher; public class TextFlowUtil { - public static TextFlow createTextFlow(String text, - float fontSize, - Font baseFont) throws IOException { + public static TextFlow createTextFlow(String text, FontDescriptor descriptor) { final Iterable parts = fromPlainText(text); - return createTextFlow(parts, fontSize, baseFont); + return createTextFlow(parts, descriptor); } /** @@ -52,41 +48,33 @@ public class TextFlowUtil { * * * @param markup the markup text. - * @param fontSize the font size to use. - * @param baseFont the font. + * @param descriptor the font size to use, and the font. * @return the created text flow. - * @throws IOException by pdfbox */ - public static TextFlow createTextFlowFromMarkup(final String markup, - final float fontSize, - Font baseFont) throws IOException { + public static TextFlow createTextFlowFromMarkup(String markup, FontDescriptor descriptor) { final Iterable parts = fromMarkup(markup); - return createTextFlow(parts, fontSize, baseFont); + return createTextFlow(parts, descriptor); } /** * Actually creates the text flow from the given (markup) text. * * @param parts the parts to create the text flow from. - * @param fontSize the font size to use. + * @param descriptor the font size to use. * @return the created text flow. - * @throws IOException by pdfbox */ - protected static TextFlow createTextFlow(final Iterable parts, - final float fontSize, Font baseFont) - throws IOException { + protected static TextFlow createTextFlow(Iterable parts, FontDescriptor descriptor) { final TextFlow result = new TextFlow(); boolean bold = false; boolean italic = false; Color color = Color.black; ControlCharacters.MetricsControlCharacter metricsControl = null; - Map, Annotation> annotationMap = new HashMap, Annotation>(); - Stack indentStack = new Stack(); + Map, Annotation> annotationMap = new HashMap<>(); + Stack indentStack = new Stack<>(); for (final CharSequence fragment : parts) { - if (fragment instanceof ControlCharacter) { if (fragment instanceof ControlCharacters.NewLineControlCharacter) { - result.add(new NewLine(fontSize)); + result.add(new NewLine(descriptor)); } if (fragment instanceof ControlCharacters.BoldControlCharacter) { bold = !bold; @@ -110,7 +98,6 @@ public class TextFlowUtil { } if (fragment instanceof ControlCharacters.MetricsControlCharacter) { if (metricsControl != null && metricsControl.toString().equals(fragment.toString())) { - // end marker metricsControl = null; } else { metricsControl = (ControlCharacters.MetricsControlCharacter) fragment; @@ -119,10 +106,8 @@ public class TextFlowUtil { if (fragment instanceof IndentCharacters.IndentCharacter) { IndentCharacters.IndentCharacter currentIndent = (IndentCharacters.IndentCharacter) fragment; if (currentIndent.getLevel() == 0) { - // indentation of 0 resets indent indentStack.clear(); result.add(Indent.UNINDENT); - continue; } else { IndentCharacters.IndentCharacter last = null; while (!indentStack.isEmpty() @@ -135,26 +120,27 @@ public class TextFlowUtil { currentIndent = last; } indentStack.push(currentIndent); - result.add(currentIndent.createNewIndent(fontSize, - baseFont.getPlainFont(), color)); + FontDescriptor fontDescriptor = + new FontDescriptor(descriptor.getFont(), descriptor.getSize(), bold, italic); + result.add(currentIndent.createNewIndent(fontDescriptor, color)); } } } else { - PDFont font = getFont(bold, italic, baseFont); float baselineOffset = 0; - float currentFontSize = fontSize; + float currentFontSize = descriptor.getSize(); if (metricsControl != null) { - baselineOffset = metricsControl.getBaselineOffsetScale() * fontSize; + baselineOffset = metricsControl.getBaselineOffsetScale() * descriptor.getSize(); currentFontSize *= metricsControl.getFontScale(); } + FontDescriptor fontDescriptor = new FontDescriptor(descriptor.getFont(), currentFontSize, bold, italic); if (annotationMap.isEmpty()) { - StyledText styledText = new StyledText(fragment.toString(), - currentFontSize, font, color, baselineOffset); + StyledText styledText = new StyledText(fragment.toString(), fontDescriptor, + color, baselineOffset, 0, 0); result.add(styledText); } else { - AnnotatedStyledText styledText = new AnnotatedStyledText( - fragment.toString(), currentFontSize, baseFont, color, baselineOffset, - annotationMap.values()); + AnnotatedStyledText styledText = + new AnnotatedStyledText(fragment.toString(), fontDescriptor, + color, baselineOffset, 0, 0, annotationMap.values()); result.add(styledText); } } @@ -162,33 +148,6 @@ public class TextFlowUtil { return result; } - protected static PDFont getFont(boolean bold, boolean italic, - final PDFont plainFont, final PDFont boldFont, - final PDFont italicFont, final PDFont boldItalicFont) { - PDFont font = plainFont; - if (bold && !italic) { - font = boldFont; - } else if (!bold && italic) { - font = italicFont; - } else if (bold) { - font = boldItalicFont; - } - return font; - } - - protected static PDFont getFont(boolean bold, boolean italic, - Font baseFont) { - PDFont font = baseFont.getPlainFont(); - if (bold && !italic) { - font = baseFont.getBoldFont(); - } else if (!bold && italic) { - font = baseFont.getItalicFont(); - } else if (bold) { - font = baseFont.getBoldItalicFont(); - } - return font; - } - /** * Creates a char sequence where new-line is replaced by the corresponding * {@link ControlCharacter}. @@ -233,25 +192,19 @@ public class TextFlowUtil { * @param markup the markup. * @return the create char sequence. */ - public static Iterable fromMarkup( - final Iterable markup) { + public static Iterable fromMarkup(Iterable markup) { Iterable text = markup; text = splitByControlCharacter(ControlCharacters.NEWLINE_FACTORY, text); text = splitByControlCharacter(ControlCharacters.METRICS_FACTORY, text); text = splitByControlCharacter(ControlCharacters.BOLD_FACTORY, text); text = splitByControlCharacter(ControlCharacters.ITALIC_FACTORY, text); text = splitByControlCharacter(ControlCharacters.COLOR_FACTORY, text); - for (AnnotationCharacters.AnnotationControlCharacterFactory annotationControlCharacterFactory : AnnotationCharacters .getFactories()) { - text = splitByControlCharacter(annotationControlCharacterFactory, - text); + text = splitByControlCharacter(annotationControlCharacterFactory, text); } - text = splitByControlCharacter(IndentCharacters.INDENT_FACTORY, text); - text = unescapeBackslash(text); - return text; } @@ -266,13 +219,12 @@ public class TextFlowUtil { protected static Iterable splitByControlCharacter( ControlCharacters.ControlCharacterFactory controlCharacterFactory, final Iterable markup) { - List result = new ArrayList(); + List result = new ArrayList<>(); boolean beginOfLine = true; for (CharSequence current : markup) { if (current instanceof String) { String string = (String) current; int begin = 0; - if (!controlCharacterFactory.patternMatchesBeginOfLine() || beginOfLine) { Matcher matcher = controlCharacterFactory.getPattern() @@ -280,24 +232,18 @@ public class TextFlowUtil { while (matcher.find()) { String part = string.substring(begin, matcher.start()); begin = matcher.end(); - if (!part.isEmpty()) { - String unescaped = controlCharacterFactory - .unescape(part); + String unescaped = controlCharacterFactory.unescape(part); result.add(unescaped); } - - result.add(controlCharacterFactory - .createControlCharacter(string, matcher, result)); + result.add(controlCharacterFactory.createControlCharacter(string, matcher, result)); } } - if (begin < string.length()) { String part = string.substring(begin); String unescaped = controlCharacterFactory.unescape(part); result.add(unescaped); } - beginOfLine = false; } else { if (current instanceof ControlCharacters.NewLineControlCharacter) { @@ -305,23 +251,19 @@ public class TextFlowUtil { } result.add(current); } - } return result; } - private static Iterable unescapeBackslash( - final Iterable chars) { - List result = new ArrayList(); + private static Iterable unescapeBackslash(Iterable chars) { + List result = new ArrayList<>(); for (CharSequence current : chars) { if (current instanceof String) { - result.add(ControlCharacters - .unescapeBackslash((String) current)); + result.add(ControlCharacters.unescapeBackslash((String) current)); } else { result.add(current); } } return result; } - } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextLine.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextLine.java index 5a37ef4..a5ceb37 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextLine.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextLine.java @@ -5,6 +5,7 @@ import org.apache.pdfbox.util.Matrix; import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import java.awt.Color; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -109,7 +110,7 @@ public class TextLine implements TextSequence { } @Override - public float getWidth() throws IOException { + public float getWidth() { Float width = getCachedValue(WIDTH, Float.class); if (width == null) { width = 0f; @@ -122,7 +123,7 @@ public class TextLine implements TextSequence { } @Override - public float getHeight() throws IOException { + public float getHeight() { Float height = getCachedValue(HEIGHT, Float.class); if (height == null) { height = 0f; @@ -136,16 +137,14 @@ public class TextLine implements TextSequence { /** * @return the (max) ascent of this line. - * @throws IOException by pdfbox. */ - protected float getAscent() throws IOException { + protected float getAscent() { Float ascent = getCachedValue(ASCENT, Float.class); if (ascent == null) { ascent = 0f; for (TextFragment fragment : this) { - float currentAscent = fragment.getFontDescriptor().getSize() - * fragment.getFontDescriptor().getFont() - .getFontDescriptor().getAscent() / 1000; + FontDescriptor fontDescriptor = fragment.getFontDescriptor(); + float currentAscent = fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getFontDescriptor().getAscent() / 1000; ascent = Math.max(ascent, currentAscent); } setCachedValue(ASCENT, ascent); @@ -155,66 +154,71 @@ public class TextLine implements TextSequence { @Override public void drawText(PDPageContentStream contentStream, Position upperLeft, - Alignment alignment, DrawListener drawListener) throws IOException { + Alignment alignment, DrawListener drawListener) { drawAligned(contentStream, upperLeft, alignment, getWidth(), drawListener); } - public void drawAligned(PDPageContentStream contentStream, Position upperLeft, - Alignment alignment, float availableLineWidth, - DrawListener drawListener) throws IOException { - contentStream.saveGraphicsState(); - float x = upperLeft.getX(); - float y = upperLeft.getY() - getAscent(); - FontDescriptor lastFontDesc = null; - float lastBaselineOffset = 0; - Color lastColor = null; - float gap = 0; - float extraWordSpacing = 0; - if (alignment == Alignment.JUSTIFY && (getNewLine() instanceof WrappingNewLine)) { - extraWordSpacing = (availableLineWidth - getWidth()) / (styledTextList.size() - 1); + public void drawAligned(PDPageContentStream contentStream, + Position upperLeft, + Alignment alignment, + float availableLineWidth, + DrawListener drawListener) { + try { + contentStream.saveGraphicsState(); + float x = upperLeft.getX(); + float y = upperLeft.getY() - getAscent(); + FontDescriptor lastFontDesc = null; + float lastBaselineOffset = 0; + Color lastColor = null; + float gap = 0; + float extraWordSpacing = 0; + if (alignment == Alignment.JUSTIFY && (getNewLine() instanceof WrappingNewLine)) { + extraWordSpacing = (availableLineWidth - getWidth()) / (styledTextList.size() - 1); + } + float offset = TextSequenceUtil.getOffset(this, availableLineWidth, alignment); + x += offset; + for (StyledText styledText : styledTextList) { + Matrix matrix = Matrix.getTranslateInstance(x, y); + if (styledText.getLeftMargin() > 0) { + gap += styledText.getLeftMargin(); + } + boolean moveBaseline = styledText.getBaselineOffset() != lastBaselineOffset; + if (moveBaseline || gap > 0) { + float baselineDelta = lastBaselineOffset - styledText.getBaselineOffset(); + lastBaselineOffset = styledText.getBaselineOffset(); + matrix = matrix.multiply(new Matrix(1, 0, 0, 1, gap, baselineDelta)); + x += gap; + } + contentStream.beginText(); + contentStream.setTextMatrix(matrix); + if (!styledText.getFontDescriptor().equals(lastFontDesc)) { + lastFontDesc = styledText.getFontDescriptor(); + contentStream.setFont(lastFontDesc.getSelectedFont(), lastFontDesc.getSize()); + } + if (!styledText.getColor().equals(lastColor)) { + lastColor = styledText.getColor(); + contentStream.setNonStrokingColor(lastColor); + } + if (styledText.getText().length() > 0) { + contentStream.showText(styledText.getText()); + } + contentStream.endText(); + if (drawListener != null) { + drawListener.drawn(styledText, + new Position(x, y + styledText.getAsent()), + styledText.getWidthWithoutMargin(), + styledText.getHeight()); + } + x += styledText.getWidthWithoutMargin(); + gap = extraWordSpacing; + if (styledText.getRightMargin() > 0) { + gap += styledText.getRightMargin(); + } + } + contentStream.restoreGraphicsState(); + } catch (IOException e) { + throw new UncheckedIOException(e); } - float offset = TextSequenceUtil.getOffset(this, availableLineWidth, alignment); - x += offset; - for (StyledText styledText : styledTextList) { - Matrix matrix = Matrix.getTranslateInstance(x, y); - if (styledText.getLeftMargin() > 0) { - gap += styledText.getLeftMargin(); - } - boolean moveBaseline = styledText.getBaselineOffset() != lastBaselineOffset; - if (moveBaseline || gap > 0) { - float baselineDelta = lastBaselineOffset - styledText.getBaselineOffset(); - lastBaselineOffset = styledText.getBaselineOffset(); - matrix = matrix.multiply(new Matrix(1, 0, 0, 1, gap, baselineDelta)); - x += gap; - } - contentStream.beginText(); - contentStream.setTextMatrix(matrix); - if (!styledText.getFontDescriptor().equals(lastFontDesc)) { - lastFontDesc = styledText.getFontDescriptor(); - contentStream.setFont(lastFontDesc.getFont(), lastFontDesc.getSize()); - } - if (!styledText.getColor().equals(lastColor)) { - lastColor = styledText.getColor(); - contentStream.setNonStrokingColor(lastColor); - } - if (styledText.getText().length() > 0) { - contentStream.showText(styledText.getText()); - } - contentStream.endText(); - if (drawListener != null) { - float currentUpperLeft = y + styledText.getAsent(); - drawListener.drawn(styledText, - new Position(x, currentUpperLeft), - styledText.getWidthWithoutMargin(), - styledText.getHeight()); - } - x += styledText.getWidthWithoutMargin(); - gap = extraWordSpacing; - if (styledText.getRightMargin() > 0) { - gap += styledText.getRightMargin(); - } - } - contentStream.restoreGraphicsState(); } @Override diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextSequenceUtil.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextSequenceUtil.java index 03da5c4..03e3dd6 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextSequenceUtil.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/TextSequenceUtil.java @@ -7,6 +7,7 @@ import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import org.xbib.graphics.pdfbox.layout.util.Pair; import org.xbib.graphics.pdfbox.layout.util.WordBreakerFactory; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; @@ -20,21 +21,16 @@ public class TextSequenceUtil { * * @param text the text to extract the lines from. * @return the list of text lines. - * @throws IOException by pdfbox */ - public static List getLines(final TextSequence text) - throws IOException { - final List result = new ArrayList(); - + public static List getLines(final TextSequence text) { + final List result = new ArrayList<>(); TextLine line = new TextLine(); for (TextFragment fragment : text) { if (fragment instanceof NewLine) { line.setNewLine((NewLine) fragment); result.add(line); line = new TextLine(); - } else if (fragment instanceof ReplacedWhitespace) { - // ignore replaced whitespace - } else { + } else if (!(fragment instanceof ReplacedWhitespace)) { line.add((StyledText) fragment); } } @@ -51,13 +47,11 @@ public class TextSequenceUtil { * @param maxWidth the max width used for word-wrapping. * @param maxHeight the max height for divide. * @return the Divided element containing the parts. - * @throws IOException by pdfbox */ public static Divided divide(final TextSequence text, final float maxWidth, - final float maxHeight) throws IOException { + final float maxHeight) { TextFlow wrapped = wordWrap(text, maxWidth); List lines = getLines(wrapped); - Paragraph first = new Paragraph(); Paragraph tail = new Paragraph(); if (text instanceof TextFlow) { @@ -74,24 +68,19 @@ public class TextSequenceUtil { tail.setAlignment(paragraph.getAlignment()); tail.setApplyLineSpacingToFirstLine(paragraph.isApplyLineSpacingToFirstLine()); } - int index = 0; do { TextLine line = lines.get(index); first.add(line); ++index; } while (index < lines.size() && first.getHeight() < maxHeight); - if (first.getHeight() > maxHeight) { - // remove last line --index; TextLine line = lines.get(index); - for (@SuppressWarnings("unused") - TextFragment textFragment : line) { + for (TextFragment textFragment : line) { first.removeLast(); } } - for (int i = index; i < lines.size(); ++i) { tail.add(lines.get(i)); } @@ -104,11 +93,8 @@ public class TextSequenceUtil { * @param text the text to word-wrap. * @param maxWidth the max width to fit. * @return the word-wrapped text. - * @throws IOException by pdfbox */ - public static TextFlow wordWrap(final TextSequence text, - final float maxWidth) throws IOException { - + public static TextFlow wordWrap(TextSequence text, float maxWidth) { float indentation = 0; TextFlow result = new TextFlow(); float lineLength = indentation; @@ -149,7 +135,7 @@ public class TextSequenceUtil { } private static WordWrapContext wordWrap(final WordWrapContext context, - final float maxWidth, final TextFlow result) throws IOException { + final float maxWidth, final TextFlow result) { TextFragment word = context.getWord(); TextFragment moreToWrap = null; float indentation = context.getIndentation(); @@ -270,9 +256,8 @@ public class TextSequenceUtil { * * @param text the text to de-wrap. * @return the de-wrapped text. - * @throws IOException by PDFBox */ - public static TextFlow deWrap(final TextSequence text) throws IOException { + public static TextFlow deWrap(final TextSequence text) { TextFlow result = new TextFlow(); for (TextFragment fragment : text) { if (fragment instanceof WrappingNewLine) { @@ -297,13 +282,10 @@ public class TextSequenceUtil { * @param text the text to word-wrap. * @param maxWidth the max width to fit. * @return the word-wrapped text lines. - * @throws IOException by pdfbox */ public static List wordWrapToLines(final TextSequence text, - final float maxWidth) throws IOException { - TextFlow wrapped = wordWrap(text, maxWidth); - List lines = getLines(wrapped); - return lines; + final float maxWidth) { + return getLines(wordWrap(text, maxWidth)); } /** @@ -367,7 +349,7 @@ public class TextSequenceUtil { private static Pair breakWord(TextFragment word, float wordWidth, final float remainingLineWidth, float maxWidth, - boolean breakHard) throws IOException { + boolean breakHard) { float leftMargin = 0; float rightMargin = 0; @@ -377,8 +359,7 @@ public class TextSequenceUtil { rightMargin = styledText.getRightMargin(); } - Pair brokenWord = WordBreakerFactory.getWorkBreaker() - .breakWord(word.getText(), word.getFontDescriptor(), + Pair brokenWord = WordBreakerFactory.getWorkBreaker().breakWord(word.getText(), word.getFontDescriptor(), remainingLineWidth - leftMargin, breakHard); if (brokenWord == null) { return null; @@ -398,10 +379,8 @@ public class TextSequenceUtil { * * @param fontDescriptor font and size. * @return the width of M. - * @throws IOException by pdfbox */ - public static float getEmWidth(final FontDescriptor fontDescriptor) - throws IOException { + public static float getEmWidth(final FontDescriptor fontDescriptor) { return getStringWidth("M", fontDescriptor); } @@ -411,12 +390,13 @@ public class TextSequenceUtil { * @param text the text to measure. * @param fontDescriptor font and size. * @return the width of given text. - * @throws IOException by pdfbox */ - public static float getStringWidth(final String text, - final FontDescriptor fontDescriptor) throws IOException { - return fontDescriptor.getSize() - * fontDescriptor.getFont().getStringWidth(text) / 1000; + public static float getStringWidth(String text, FontDescriptor fontDescriptor) { + try { + return fontDescriptor.getSize() * fontDescriptor.getSelectedFont().getStringWidth(text) / 1000; + } catch (IOException exception) { + throw new UncheckedIOException(exception); + } } /** @@ -434,13 +414,13 @@ public class TextSequenceUtil { * @param lineSpacing the line spacing factor. * @param applyLineSpacingToFirstLine indicates if the line spacing should be applied to the first * line also. Makes sense in most cases to do so. - * @throws IOException by pdfbox */ public static void drawText(TextSequence text, - PDPageContentStream contentStream, Position upperLeft, - DrawListener drawListener, Alignment alignment, float maxWidth, - final float lineSpacing, final boolean applyLineSpacingToFirstLine) - throws IOException { + PDPageContentStream contentStream, + Position upperLeft, + DrawListener drawListener, + Alignment alignment, + float maxWidth, final float lineSpacing, final boolean applyLineSpacingToFirstLine) { List lines = wordWrapToLines(text, maxWidth); float maxLineWidth = Math.max(maxWidth, getMaxWidth(lines)); Position position = upperLeft; @@ -468,11 +448,9 @@ public class TextSequenceUtil { * @param targetWidth the target width * @param alignment the alignment of the line. * @return the left offset. - * @throws IOException by pdfbox */ public static float getOffset(final TextSequence textLine, - final float targetWidth, final Alignment alignment) - throws IOException { + final float targetWidth, final Alignment alignment) { switch (alignment) { case RIGHT: return targetWidth - textLine.getWidth(); @@ -488,10 +466,8 @@ public class TextSequenceUtil { * * @param lines the lines for which to calculate the max width. * @return the max width of the lines. - * @throws IOException by pdfbox. */ - public static float getMaxWidth(final Iterable lines) - throws IOException { + public static float getMaxWidth(Iterable lines) { float max = 0; for (TextLine line : lines) { max = Math.max(max, line.getWidth()); @@ -505,10 +481,8 @@ public class TextSequenceUtil { * @param textSequence the text. * @param maxWidth if > 0, the text may be word-wrapped to match the width. * @return the width of the text. - * @throws IOException by pdfbox. */ - public static float getWidth(final TextSequence textSequence, - final float maxWidth) throws IOException { + public static float getWidth(TextSequence textSequence, float maxWidth) { List lines = wordWrapToLines(textSequence, maxWidth); float max = 0; for (TextLine line : lines) { @@ -526,11 +500,10 @@ public class TextSequenceUtil { * @param applyLineSpacingToFirstLine indicates if the line spacing should be applied to the first * line also. Makes sense in most cases to do so. * @return the height of the text. - * @throws IOException by pdfbox */ public static float getHeight(final TextSequence textSequence, final float maxWidth, final float lineSpacing, - final boolean applyLineSpacingToFirstLine) throws IOException { + final boolean applyLineSpacingToFirstLine) { List lines = wordWrapToLines(textSequence, maxWidth); float sum = 0; for (int i = 0; i < lines.size(); i++) { @@ -546,9 +519,13 @@ public class TextSequenceUtil { } private static class WordWrapContext { + private final TextFragment word; + private final float lineLength; + private final float indentation; + boolean isWrappedLine; public WordWrapContext(TextFragment word, float lineLength, @@ -578,6 +555,5 @@ public class TextSequenceUtil { public boolean isMoreToWrap() { return getWord() != null; } - } } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/WrappingNewLine.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/WrappingNewLine.java index efc1b6f..97d6607 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/WrappingNewLine.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/WrappingNewLine.java @@ -1,6 +1,5 @@ package org.xbib.graphics.pdfbox.layout.text; - import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; /** @@ -9,13 +8,6 @@ import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; */ public class WrappingNewLine extends NewLine { - /** - * See {@link NewLine#NewLine()}. - */ - public WrappingNewLine() { - super(); - } - /** * See {@link NewLine#NewLine(FontDescriptor)}. * @@ -24,15 +16,4 @@ public class WrappingNewLine extends NewLine { public WrappingNewLine(FontDescriptor fontDescriptor) { super(fontDescriptor); } - - /** - * See {@link NewLine#NewLine(float)}. - * - * @param fontSize the font size, resp. the height of the new line. - */ - public WrappingNewLine(float fontSize) { - super(fontSize); - } - - } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotatedStyledText.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotatedStyledText.java index ce14a26..349d852 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotatedStyledText.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotatedStyledText.java @@ -17,23 +17,6 @@ public class AnnotatedStyledText extends StyledText implements Annotated { private final List 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 annotations) { - this(text, new FontDescriptor(font.getPlainFont(), size), color, baselineOffset, 0, 0, annotations); - } - /** * Creates a styled text. * @@ -52,8 +35,7 @@ public class AnnotatedStyledText extends StyledText implements Annotated { final float rightMargin, final float baselineOffset, Collection annotations) { - super(text, fontDescriptor, color, baselineOffset, leftMargin, - rightMargin); + super(text, fontDescriptor, color, baselineOffset, leftMargin, rightMargin); if (annotations != null) { this.annotations.addAll(annotations); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationCharacters.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationCharacters.java index 152f001..7897331 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationCharacters.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationCharacters.java @@ -6,8 +6,8 @@ import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.AnchorAnnota import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation.LinkStyle; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.UnderlineAnnotation; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -16,7 +16,7 @@ import java.util.regex.Pattern; */ public class AnnotationCharacters { - private final static List> FACTORIES = new CopyOnWriteArrayList<>(); + private final static List> FACTORIES = new ArrayList<>(); static { register(new HyperlinkControlCharacterFactory()); @@ -30,8 +30,7 @@ public class AnnotationCharacters { * * @param factory the factory to register. */ - public static void register( - final AnnotationControlCharacterFactory factory) { + public static void register(AnnotationControlCharacterFactory factory) { FACTORIES.add(factory); } @@ -97,8 +96,7 @@ public class AnnotationCharacters { @Override public String unescape(String text) { - return text - .replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE); + return text.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE); } @Override @@ -119,8 +117,7 @@ public class AnnotationCharacters { @Override public UnderlineControlCharacter createControlCharacter(String text, Matcher matcher, final List charactersSoFar) { - return new UnderlineControlCharacter(matcher.group(4), - matcher.group(6)); + return new UnderlineControlCharacter(matcher.group(4), matcher.group(6)); } @Override @@ -130,8 +127,7 @@ public class AnnotationCharacters { @Override public String unescape(String text) { - return text - .replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE); + return text.replaceAll("\\\\" + Pattern.quote(TO_ESCAPE), TO_ESCAPE); } @Override @@ -149,6 +145,7 @@ public class AnnotationCharacters { */ public static class HyperlinkControlCharacter extends AnnotationControlCharacter { + private HyperlinkAnnotation hyperlink; protected HyperlinkControlCharacter(final String hyperlink, diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationDrawListener.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationDrawListener.java index db32d52..8ef4c9b 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationDrawListener.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationDrawListener.java @@ -9,7 +9,6 @@ import org.xbib.graphics.pdfbox.layout.text.DrawContext; import org.xbib.graphics.pdfbox.layout.text.DrawListener; import org.xbib.graphics.pdfbox.layout.text.DrawableText; import org.xbib.graphics.pdfbox.layout.text.Position; -import java.io.IOException; /** * This listener has to be passed to all @@ -44,59 +43,35 @@ public class AnnotationDrawListener implements DrawListener, RenderListener { return; } for (AnnotationProcessor annotationProcessor : annotationProcessors) { - try { - annotationProcessor.annotatedObjectDrawn( - (Annotated) drawnObject, drawContext, upperLeft, width, - height); - } catch (IOException e) { - throw new RuntimeException( - "exception on annotation processing", e); - } + annotationProcessor.annotatedObjectDrawn( + (Annotated) drawnObject, drawContext, upperLeft, width, + height); } } - /** - * @throws IOException by pdfbox. - * @deprecated user {@link #afterRender()} instead. - */ @Deprecated - public void finalizeAnnotations() throws IOException { + public void finalizeAnnotations() { afterRender(); } @Override - public void beforePage(RenderContext renderContext) throws IOException { + public void beforePage(RenderContext renderContext) { for (AnnotationProcessor annotationProcessor : annotationProcessors) { - try { - annotationProcessor.beforePage(drawContext); - } catch (IOException e) { - throw new RuntimeException( - "exception on annotation processing", e); - } + annotationProcessor.beforePage(drawContext); } } @Override - public void afterPage(RenderContext renderContext) throws IOException { + public void afterPage(RenderContext renderContext) { for (AnnotationProcessor annotationProcessor : annotationProcessors) { - try { - annotationProcessor.afterPage(drawContext); - } catch (IOException e) { - throw new RuntimeException( - "exception on annotation processing", e); - } + annotationProcessor.afterPage(drawContext); } } - public void afterRender() throws IOException { + public void afterRender() { for (AnnotationProcessor annotationProcessor : annotationProcessors) { - try { - annotationProcessor.afterRender(drawContext.getPdDocument()); - } catch (IOException e) { - throw new RuntimeException( - "exception on annotation processing", e); - } + annotationProcessor.afterRender(drawContext.getPdDocument()); } } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationProcessor.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationProcessor.java index 02c3266..7ac2091 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationProcessor.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/AnnotationProcessor.java @@ -3,7 +3,6 @@ package org.xbib.graphics.pdfbox.layout.text.annotations; import org.apache.pdfbox.pdmodel.PDDocument; import org.xbib.graphics.pdfbox.layout.text.DrawContext; import org.xbib.graphics.pdfbox.layout.text.Position; -import java.io.IOException; /** * Processes an annotation. @@ -18,34 +17,30 @@ public interface AnnotationProcessor { * @param upperLeft the upper left position the object has been drawn to. * @param width the width of the drawn object. * @param height the height of the drawn object. - * @throws IOException by pdfbox. */ void annotatedObjectDrawn(final Annotated drawnObject, final DrawContext drawContext, Position upperLeft, float width, - float height) throws IOException; + float height); /** * Called before a page is drawn. * * @param drawContext the drawing context. - * @throws IOException by pdfbox. */ - void beforePage(final DrawContext drawContext) throws IOException; + void beforePage(final DrawContext drawContext); /** * Called after a page is drawn. * * @param drawContext the drawing context. - * @throws IOException by pdfbox. */ - void afterPage(final DrawContext drawContext) throws IOException; + void afterPage(final DrawContext drawContext); /** * Called after all rendering has been performed. * * @param document the document. - * @throws IOException by pdfbox. */ - void afterRender(final PDDocument document) throws IOException; + void afterRender(final PDDocument document); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/HyperlinkAnnotationProcessor.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/HyperlinkAnnotationProcessor.java index 7152d4a..1d0f670 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/HyperlinkAnnotationProcessor.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/HyperlinkAnnotationProcessor.java @@ -3,16 +3,23 @@ package org.xbib.graphics.pdfbox.layout.text.annotations; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionGoTo; +import org.apache.pdfbox.pdmodel.interactive.action.PDActionURI; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationLink; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDBorderStyleDictionary; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDDestination; import org.apache.pdfbox.pdmodel.interactive.documentnavigation.destination.PDPageXYZDestination; import org.xbib.graphics.pdfbox.layout.text.DrawContext; import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.AnchorAnnotation; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.HyperlinkAnnotation.LinkStyle; -import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper; import java.awt.Color; +import java.awt.geom.AffineTransform; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -26,13 +33,13 @@ import java.util.Map.Entry; */ public class HyperlinkAnnotationProcessor implements AnnotationProcessor { - private final Map anchorMap = new HashMap(); - private final Map> linkMap = new HashMap>(); + private final Map anchorMap = new HashMap<>(); + private final Map> linkMap = new HashMap<>(); @Override public void annotatedObjectDrawn(Annotated drawnObject, DrawContext drawContext, Position upperLeft, float width, - float height) throws IOException { + float height) { if (!(drawnObject instanceof AnnotatedStyledText)) { return; @@ -85,26 +92,107 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor { } @Override - public void afterRender(PDDocument document) throws IOException { + public void afterRender(PDDocument document) { for (Entry> entry : linkMap.entrySet()) { PDPage page = entry.getKey(); List links = entry.getValue(); for (Hyperlink hyperlink : links) { - PDAnnotationLink pdLink = null; + PDAnnotationLink pdLink; if (hyperlink.getHyperlinkURI().startsWith("#")) { pdLink = createGotoLink(hyperlink); } else { - pdLink = CompatibilityHelper.createLink(page, + pdLink = createLink(page, hyperlink.getRect(), hyperlink.getColor(), hyperlink.getLinkStyle(), hyperlink.getHyperlinkURI()); } - page.getAnnotations().add(pdLink); + try { + page.getAnnotations().add(pdLink); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } } } + private static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color, + LinkStyle linkStyle, final String uri) { + PDAnnotationLink pdLink = createLink(page, rect, color, linkStyle); + PDActionURI actionUri = new PDActionURI(); + actionUri.setURI(uri); + pdLink.setAction(actionUri); + return pdLink; + } + + private static PDBorderStyleDictionary noBorder; + + private static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color, + LinkStyle linkStyle) { + PDAnnotationLink pdLink = new PDAnnotationLink(); + if (linkStyle == LinkStyle.none) { + if (noBorder == null) { + noBorder = new PDBorderStyleDictionary(); + noBorder.setWidth(0); + } + return pdLink; + } + PDBorderStyleDictionary borderStyle = new PDBorderStyleDictionary(); + borderStyle.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE); + pdLink.setBorderStyle(borderStyle); + PDRectangle rotatedRect = transformToPageRotation(rect, page); + pdLink.setRectangle(rotatedRect); + pdLink.setColor(toPDColor(color)); + return pdLink; + } + + private static PDAnnotationLink createLink(PDPage page, PDRectangle rect, Color color, + LinkStyle linkStyle, final PDDestination destination) { + PDAnnotationLink pdLink = createLink(page, rect, color, linkStyle); + PDActionGoTo gotoAction = new PDActionGoTo(); + gotoAction.setDestination(destination); + pdLink.setAction(gotoAction); + return pdLink; + } + + private static PDRectangle transformToPageRotation(final PDRectangle rect, final PDPage page) { + AffineTransform transform = transformToPageRotation(page); + if (transform == null) { + return rect; + } + float[] points = new float[]{rect.getLowerLeftX(), rect.getLowerLeftY(), rect.getUpperRightX(), rect.getUpperRightY()}; + float[] rotatedPoints = new float[4]; + transform.transform(points, 0, rotatedPoints, 0, 2); + PDRectangle rotated = new PDRectangle(); + rotated.setLowerLeftX(rotatedPoints[0]); + rotated.setLowerLeftY(rotatedPoints[1]); + rotated.setUpperRightX(rotatedPoints[2]); + rotated.setUpperRightY(rotatedPoints[3]); + return rotated; + } + + private static AffineTransform transformToPageRotation(final PDPage page) { + int pageRotation = page.getRotation(); + if (pageRotation == 0) { + return null; + } + float pageWidth = page.getMediaBox().getHeight(); + float pageHeight = page.getMediaBox().getWidth(); + AffineTransform transform = new AffineTransform(); + transform.rotate(pageRotation * Math.PI / 180, pageHeight / 2, + pageWidth / 2); + double offset = Math.abs(pageHeight - pageWidth) / 2; + transform.translate(-offset, offset); + return transform; + } + + private static PDColor toPDColor(final Color color) { + float[] components = { + color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f + }; + return new PDColor(components, PDDeviceRGB.INSTANCE); + } + private PDAnnotationLink createGotoLink(Hyperlink hyperlink) { String anchor = hyperlink.getHyperlinkURI().substring(1); PageAnchor pageAnchor = anchorMap.get(anchor); @@ -116,7 +204,7 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor { xyzDestination.setPage(pageAnchor.getPage()); xyzDestination.setLeft((int) pageAnchor.getX()); xyzDestination.setTop((int) pageAnchor.getY()); - return CompatibilityHelper.createLink(pageAnchor.getPage(), hyperlink.getRect(), + return createLink(pageAnchor.getPage(), hyperlink.getRect(), hyperlink.getColor(), hyperlink.getLinkStyle(), xyzDestination); } @@ -147,7 +235,6 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor { public String toString() { return "PageAnchor [page=" + page + ", x=" + x + ", y=" + y + "]"; } - } private static class Hyperlink { @@ -186,7 +273,5 @@ public class HyperlinkAnnotationProcessor implements AnnotationProcessor { + ", hyperlinkUri=" + hyperlinkUri + ", linkStyle=" + linkStyle + "]"; } - } - } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/UnderlineAnnotationProcessor.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/UnderlineAnnotationProcessor.java index 44cc38d..5b0ff72 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/UnderlineAnnotationProcessor.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/text/annotations/UnderlineAnnotationProcessor.java @@ -9,11 +9,12 @@ import org.xbib.graphics.pdfbox.layout.text.StyledText; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotations.UnderlineAnnotation; import java.awt.Color; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; /** - * This annotation processor handles the {@link UnderlineAnnotation}s, and adds + * This annotation processor handles the {@link UnderlineAnnotation} and adds * the needed hyperlink metadata to the PDF document. */ public class UnderlineAnnotationProcessor implements AnnotationProcessor { @@ -22,23 +23,20 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor { @Override public void annotatedObjectDrawn(Annotated drawnObject, - DrawContext drawContext, Position upperLeft, float width, + DrawContext drawContext, + Position upperLeft, + float width, float height) { if (!(drawnObject instanceof StyledText)) { return; } StyledText drawnText = (StyledText) drawnObject; - for (UnderlineAnnotation underlineAnnotation : drawnObject - .getAnnotationsOfType(UnderlineAnnotation.class)) { + for (UnderlineAnnotation underlineAnnotation : drawnObject.getAnnotationsOfType(UnderlineAnnotation.class)) { float fontSize = drawnText.getFontDescriptor().getSize(); - float ascent = fontSize - * drawnText.getFontDescriptor().getFont() - .getFontDescriptor().getAscent() / 1000; + float ascent = fontSize * drawnText.getFontDescriptor().getSelectedFont().getFontDescriptor().getAscent() / 1000; float baselineOffset = fontSize * underlineAnnotation.getBaselineOffsetScale(); - float thickness = (0.01f + fontSize * 0.05f) - * underlineAnnotation.getLineWeight(); - Position start = new Position(upperLeft.getX(), upperLeft.getY() - - ascent + baselineOffset); + float thickness = (0.01f + fontSize * 0.05f) * underlineAnnotation.getLineWeight(); + Position start = new Position(upperLeft.getX(), upperLeft.getY() - ascent + baselineOffset); Position end = new Position(start.getX() + width, start.getY()); Stroke stroke = Stroke.builder().lineWidth(thickness).build(); Line line = new Line(start, end, stroke, drawnText.getColor()); @@ -47,12 +45,12 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor { } @Override - public void beforePage(DrawContext drawContext) throws IOException { + public void beforePage(DrawContext drawContext) { linesOnPage.clear(); } @Override - public void afterPage(DrawContext drawContext) throws IOException { + public void afterPage(DrawContext drawContext) { for (Line line : linesOnPage) { line.draw(drawContext.getCurrentPageContentStream()); } @@ -60,15 +58,18 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor { } @Override - public void afterRender(PDDocument document) throws IOException { + public void afterRender(PDDocument document) { linesOnPage.clear(); } private static class Line { private final Position start; + private final Position end; + private final Stroke stroke; + private final Color color; public Line(Position start, Position end, Stroke stroke, Color color) { @@ -79,15 +80,19 @@ public class UnderlineAnnotationProcessor implements AnnotationProcessor { this.color = color; } - public void draw(PDPageContentStream contentStream) throws IOException { - if (color != null) { - contentStream.setStrokingColor(color); + public void draw(PDPageContentStream contentStream) { + try { + if (color != null) { + contentStream.setStrokingColor(color); + } + if (stroke != null) { + stroke.applyTo(contentStream); + } + contentStream.moveTo(start.getX(), start.getY()); + contentStream.lineTo(end.getX(), end.getY()); + } catch (IOException e) { + throw new UncheckedIOException(e); } - if (stroke != null) { - stroke.applyTo(contentStream); - } - contentStream.moveTo(start.getX(), start.getY()); - contentStream.lineTo(end.getX(), end.getY()); } } } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/CompatibilityHelper.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/CompatibilityHelper.java deleted file mode 100644 index 29d5fee..0000000 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/CompatibilityHelper.java +++ /dev/null @@ -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 pdfbox.layout.bullet.odd and/or - * pdfbox.layout.bullet.even. - * - * @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; - } -} diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/EnumeratorFactory.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/EnumeratorFactory.java index 697afc3..7cc1b56 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/EnumeratorFactory.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/EnumeratorFactory.java @@ -5,8 +5,8 @@ import org.xbib.graphics.pdfbox.layout.util.Enumerators.ArabicEnumerator; import org.xbib.graphics.pdfbox.layout.util.Enumerators.LowerCaseAlphabeticEnumerator; import org.xbib.graphics.pdfbox.layout.util.Enumerators.LowerCaseRomanEnumerator; import org.xbib.graphics.pdfbox.layout.util.Enumerators.RomanEnumerator; +import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** * Enumerators are created using this factory. It allows you to register and use @@ -23,7 +23,7 @@ import java.util.concurrent.ConcurrentHashMap; */ public class EnumeratorFactory { - private final static Map> ENUMERATORS = new ConcurrentHashMap>(); + private final static Map> ENUMERATORS = new HashMap<>(); static { register("1", ArabicEnumerator.class); diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreaker.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreaker.java index 044aaa9..ff4baad 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreaker.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreaker.java @@ -23,6 +23,6 @@ public interface WordBreaker { */ Pair breakWord(String word, FontDescriptor fontDescriptor, float maxWidth, - boolean breakHardIfNecessary) throws IOException; + boolean breakHardIfNecessary); } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreakerFactory.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreakerFactory.java index 4408230..f99dd52 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreakerFactory.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreakerFactory.java @@ -2,8 +2,8 @@ package org.xbib.graphics.pdfbox.layout.util; import org.xbib.graphics.pdfbox.layout.util.WordBreakers.DefaultWordBreaker; import org.xbib.graphics.pdfbox.layout.util.WordBreakers.NonBreakingWordBreaker; +import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; /** * Factory for creating a {@link WordBreaker}. This may be used to define a @@ -21,12 +21,6 @@ public class WordBreakerFactory { */ public final static String WORD_BREAKER_CLASS_PROPERTY = "pdfbox.layout.word.breaker"; - /** - * class name of the default word breaker. - */ - public final static String DEFAULT_WORD_BREAKER_CLASS_NAME = DefaultWordBreaker.class - .getName(); - /** * class name of the (legacy) non-breaking word breaker. */ @@ -34,7 +28,8 @@ public class WordBreakerFactory { .getName(); private final static WordBreaker DEFAULT_WORD_BREAKER = new DefaultWordBreaker(); - private final static Map WORD_BREAKERS = new ConcurrentHashMap(); + + private final static Map WORD_BREAKERS = new HashMap<>(); /** * @return the word breaker instance to use. @@ -59,8 +54,7 @@ public class WordBreakerFactory { try { return (WordBreaker) Class.forName(className).getDeclaredConstructor().newInstance(); } catch (Exception e) { - throw new RuntimeException(String.format( - "failed to create word breaker '%s'", className), e); + throw new RuntimeException(String.format("failed to create word breaker '%s'", className), e); } } } diff --git a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreakers.java b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreakers.java index 5f2d129..805870a 100644 --- a/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreakers.java +++ b/graphics-pdfbox-layout/src/main/java/org/xbib/graphics/pdfbox/layout/util/WordBreakers.java @@ -2,7 +2,6 @@ package org.xbib.graphics.pdfbox.layout.util; import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import org.xbib.graphics.pdfbox.layout.text.TextSequenceUtil; -import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -19,7 +18,7 @@ public class WordBreakers { @Override public Pair breakWord(String word, FontDescriptor fontDescriptor, float maxWidth, - boolean breakHardIfNecessary) throws IOException { + boolean breakHardIfNecessary) { return null; } @@ -36,7 +35,7 @@ public class WordBreakers { @Override public Pair breakWord(final String word, final FontDescriptor fontDescriptor, final float maxWidth, - final boolean breakHardIfNecessary) throws IOException { + final boolean breakHardIfNecessary) { Pair brokenWord = breakWordSoft(word, fontDescriptor, maxWidth); @@ -54,11 +53,9 @@ public class WordBreakers { * @param fontDescriptor describing the font's type and size. * @param maxWidth the maximum width to obey. * @return the broken word, or null if it cannot be broken. - * @throws IOException by pdfbox */ - abstract protected Pair breakWordSoft(final String word, - final FontDescriptor fontDescriptor, final float maxWidth) - throws IOException; + abstract protected Pair breakWordSoft(String word, + FontDescriptor fontDescriptor, final float maxWidth); /** * Breaks the word hard at the outermost position that fits the given @@ -68,11 +65,9 @@ public class WordBreakers { * @param fontDescriptor describing the font's type and size. * @param maxWidth the maximum width to obey. * @return the broken word, or null if it cannot be broken. - * @throws IOException by pdfbox */ protected Pair breakWordHard(final String word, - final FontDescriptor fontDescriptor, final float maxWidth) - throws IOException { + final FontDescriptor fontDescriptor, final float maxWidth) { int cutIndex = (int) (maxWidth / TextSequenceUtil.getEmWidth(fontDescriptor)); float currentWidth = TextSequenceUtil.getStringWidth(word.substring(0, cutIndex), fontDescriptor); @@ -114,13 +109,12 @@ public class WordBreakers { * A letter followed by either -, ., * , or /. */ - private final Pattern breakPattern = Pattern - .compile("[A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]([\\-\\.\\,/])"); + private final Pattern breakPattern = + Pattern.compile("[A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u00FF]([\\-\\.\\,/])"); @Override protected Pair breakWordSoft(final String word, - final FontDescriptor fontDescriptor, final float maxWidth) - throws IOException { + final FontDescriptor fontDescriptor, final float maxWidth) { Matcher matcher = breakPattern.matcher(word); int breakIndex = -1; boolean maxWidthExceeded = false; @@ -139,7 +133,7 @@ public class WordBreakers { if (breakIndex < 0) { return null; } - return new Pair(word.substring(0, breakIndex), + return new Pair<>(word.substring(0, breakIndex), word.substring(breakIndex)); } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomAnnotationTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomAnnotationTest.java index 0c50aff..b51a572 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomAnnotationTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomAnnotationTest.java @@ -1,13 +1,17 @@ package org.xbib.graphics.pdfbox.layout.test; import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; import org.apache.pdfbox.pdmodel.interactive.annotation.PDAnnotationTextMarkup; import org.junit.jupiter.api.Test; import org.xbib.graphics.pdfbox.layout.elements.Document; import org.xbib.graphics.pdfbox.layout.elements.PageFormat; import org.xbib.graphics.pdfbox.layout.elements.Paragraph; import org.xbib.graphics.pdfbox.layout.font.BaseFont; +import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import org.xbib.graphics.pdfbox.layout.text.DrawContext; import org.xbib.graphics.pdfbox.layout.text.Position; import org.xbib.graphics.pdfbox.layout.text.annotations.Annotated; @@ -18,11 +22,12 @@ import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationCharacters.Ann import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationCharacters.AnnotationControlCharacterFactory; import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationProcessor; import org.xbib.graphics.pdfbox.layout.text.annotations.AnnotationProcessorFactory; -import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper; import java.awt.Color; +import java.awt.geom.AffineTransform; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.UncheckedIOException; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; @@ -57,17 +62,13 @@ public class CustomAnnotationTest { @Override public void annotatedObjectDrawn(Annotated drawnObject, DrawContext drawContext, Position upperLeft, float width, - float height) throws IOException { - + float height) { Iterable HighlightAnnotations = drawnObject .getAnnotationsOfType(HighlightAnnotation.class); - for (HighlightAnnotation highlightAnnotation : HighlightAnnotations) { - // use PDF text markup to implement the highlight PDAnnotationTextMarkup markup = new PDAnnotationTextMarkup( PDAnnotationTextMarkup.SUB_TYPE_HIGHLIGHT); - // use the bounding box of the drawn object to position the // highlight PDRectangle bounds = new PDRectangle(); @@ -76,33 +77,34 @@ public class CustomAnnotationTest { bounds.setUpperRightX(upperLeft.getX() + width); bounds.setUpperRightY(upperLeft.getY() + 1); markup.setRectangle(bounds); - float[] quadPoints = CompatibilityHelper.toQuadPoints(bounds); - quadPoints = CompatibilityHelper.transformToPageRotation( - quadPoints, drawContext.getCurrentPage()); + float[] quadPoints = toQuadPoints(bounds); + quadPoints = transformToPageRotation(quadPoints, drawContext.getCurrentPage()); markup.setQuadPoints(quadPoints); - // set the highlight color if given if (highlightAnnotation.getColor() != null) { - CompatibilityHelper.setAnnotationColor(markup, highlightAnnotation.getColor()); + markup.setColor(toPDColor(highlightAnnotation.getColor())); } - // finally add the markup to the PDF - drawContext.getCurrentPage().getAnnotations().add(markup); + try { + drawContext.getCurrentPage().getAnnotations().add(markup); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } } @Override - public void beforePage(DrawContext drawContext) throws IOException { + public void beforePage(DrawContext drawContext) { // nothing to do here for us } @Override - public void afterPage(DrawContext drawContext) throws IOException { + public void afterPage(DrawContext drawContext) { // nothing to do here for us } @Override - public void afterRender(PDDocument document) throws IOException { + public void afterRender(PDDocument document) { // nothing to do here for us } @@ -113,8 +115,7 @@ public class CustomAnnotationTest { * contains any information passed by the markup necessary for rendering, in * our case here it is just the color for the highlight. */ - public static class HighlightControlCharacter extends - AnnotationControlCharacter { + public static class HighlightControlCharacter extends AnnotationControlCharacter { private final HighlightAnnotation annotation; @@ -195,31 +196,88 @@ public class CustomAnnotationTest { // now add some annotated text using our custom highlight annotation HighlightAnnotation annotation = new HighlightAnnotation(Color.green); + FontDescriptor fontDescriptor = new FontDescriptor(BaseFont.HELVETICA, 10); AnnotatedStyledText highlightedText = new AnnotatedStyledText( - "highlighted text", 10, BaseFont.HELVETICA, Color.black, 0f, + "highlighted text", fontDescriptor, Color.black, 0f, 0, 0, Collections.singleton(annotation)); paragraph.add(highlightedText); - - paragraph - .addText( - ". Do whatever you want here...strike, squiggle, whatsoever\n\n", + paragraph.addText(". Do whatever you want here...strike, squiggle, whatsoever\n\n", 10, BaseFont.HELVETICA); paragraph.setMaxWidth(150); document.add(paragraph); // register markup processing for the highlight annotation AnnotationCharacters.register(new HighlightControlCharacterFactory()); - paragraph = new Paragraph(); - paragraph - .addMarkup( - "Hello there, here is {hl:#ffff00}highlighted text{hl}. " + paragraph.addMarkup("Hello there, here is {hl:#ffff00}highlighted text{hl}. " + "Do whatever you want here...strike, squiggle, whatsoever\n\n", 10, BaseFont.HELVETICA); paragraph.setMaxWidth(150); document.add(paragraph); - final OutputStream outputStream = new FileOutputStream("build/customannotation.pdf"); document.save(outputStream); } + + 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 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; + } + + 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; + } + + /** + * 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; + } } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomRenderer.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomRenderer.java index 2536a0e..9820f31 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomRenderer.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/CustomRenderer.java @@ -10,6 +10,7 @@ import org.xbib.graphics.pdfbox.layout.elements.render.RenderContext; import org.xbib.graphics.pdfbox.layout.elements.render.RenderListener; import org.xbib.graphics.pdfbox.layout.elements.render.Renderer; import org.xbib.graphics.pdfbox.layout.elements.render.VerticalLayoutHint; +import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import org.xbib.graphics.pdfbox.layout.shape.Stroke; import org.xbib.graphics.pdfbox.layout.shape.Stroke.CapStyle; import org.xbib.graphics.pdfbox.layout.text.Alignment; @@ -80,19 +81,14 @@ public class CustomRenderer { public boolean render(RenderContext renderContext, Element element, LayoutHint layoutHint) throws IOException { if (element instanceof Section) { - if (renderContext.getPageIndex() > 0) { - // no new page on first page ;-) renderContext.newPage(); } sectionNumber = ((Section) element).getNumber(); - renderContext.render(renderContext, element, layoutHint); - Element ruler = new HorizontalRuler(Stroke.builder().lineWidth(2) .capStyle(CapStyle.RoundCap).build(), Color.black); renderContext.render(renderContext, ruler, VerticalLayoutHint.builder().marginBottom(10).build()); - return true; } return false; @@ -100,20 +96,17 @@ public class CustomRenderer { @Override public void beforePage(RenderContext renderContext) { - } @Override - public void afterPage(RenderContext renderContext) throws IOException { - String content = String.format("Section %s, Page %s", - sectionNumber, renderContext.getPageIndex() + 1); - TextFlow text = TextFlowUtil.createTextFlow(content, 11, BaseFont.TIMES); + public void afterPage(RenderContext renderContext) { + String content = String.format("Section %s, Page %s", sectionNumber, renderContext.getPageIndex() + 1); + FontDescriptor fontDescriptor = new FontDescriptor(BaseFont.TIMES, 11); + TextFlow text = TextFlowUtil.createTextFlow(content, fontDescriptor); float offset = renderContext.getPageFormat().getMarginLeft() + TextSequenceUtil.getOffset(text, renderContext.getWidth(), Alignment.RIGHT); text.drawText(renderContext.getContentStream(), new Position( offset, 30), Alignment.RIGHT, null); } - } - } \ No newline at end of file diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/IndentationTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/IndentationTest.java index 21e528c..2f6675a 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/IndentationTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/IndentationTest.java @@ -1,6 +1,5 @@ package org.xbib.graphics.pdfbox.layout.test; -import org.apache.pdfbox.pdmodel.font.PDType1Font; import org.junit.jupiter.api.Test; import org.xbib.graphics.pdfbox.layout.elements.Document; import org.xbib.graphics.pdfbox.layout.elements.Paragraph; @@ -8,7 +7,6 @@ import org.xbib.graphics.pdfbox.layout.text.Alignment; import org.xbib.graphics.pdfbox.layout.font.BaseFont; import org.xbib.graphics.pdfbox.layout.text.Indent; import org.xbib.graphics.pdfbox.layout.text.SpaceUnit; -import org.xbib.graphics.pdfbox.layout.util.CompatibilityHelper; import org.xbib.graphics.pdfbox.layout.util.Enumerators.AlphabeticEnumerator; import org.xbib.graphics.pdfbox.layout.util.Enumerators.ArabicEnumerator; import org.xbib.graphics.pdfbox.layout.util.Enumerators.LowerCaseAlphabeticEnumerator; @@ -21,13 +19,13 @@ public class IndentationTest { @Test public void test() throws Exception { - String bulletOdd = CompatibilityHelper.getBulletCharacter(1) + " "; - String bulletEven = CompatibilityHelper.getBulletCharacter(2) + " "; + String bulletOdd = getBulletCharacter(1) + " "; + String bulletEven = getBulletCharacter(2) + " "; Document document = new Document(40, 60, 40, 60); Paragraph paragraph = new Paragraph(); paragraph.addMarkup("This is an example for the new indent feature. Let's do some empty space indentation:\n", - 11, BaseFont.TIMES); + 11, BaseFont.TIMES); paragraph.add(new Indent(50, SpaceUnit.pt)); paragraph.addMarkup("Here we go indented.\n", 11, BaseFont.TIMES); paragraph.addMarkup("The Indentation holds for the rest of the paragraph, or... \n", @@ -37,89 +35,75 @@ public class IndentationTest { document.add(paragraph); paragraph = new Paragraph(); - paragraph - .addMarkup( - "New paragraph, now indentation is gone. But we can indent with a label also:\n", - 11, BaseFont.TIMES); - paragraph.add(new Indent("This is some label", 100, SpaceUnit.pt, 11, - PDType1Font.TIMES_BOLD)); + paragraph.addMarkup("New paragraph, now indentation is gone. But we can indent with a label also:\n", 11, BaseFont.TIMES); + paragraph.addIndent("This is some label", 100, SpaceUnit.pt, 11, BaseFont.TIMES); paragraph.addMarkup("Here we go indented.\n", 11, BaseFont.TIMES); - paragraph - .addMarkup( - "And again, the Indentation holds for the rest of the paragraph, or any new indent comes.\nLabels can be aligned:\n", - 11, BaseFont.TIMES); - paragraph.add(new Indent("Left", 100, SpaceUnit.pt, 11, - PDType1Font.TIMES_BOLD, Alignment.LEFT)); - paragraph.addMarkup("Indent with label aligned to the left.\n", 11, - BaseFont.TIMES); - paragraph.add(new Indent("Center", 100, SpaceUnit.pt, 11, - PDType1Font.TIMES_BOLD, Alignment.CENTER)); - paragraph.addMarkup("Indent with label aligned to the center.\n", 11, - BaseFont.TIMES); - paragraph.add(new Indent("Right", 100, SpaceUnit.pt, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); - paragraph.addMarkup("Indent with label aligned to the right.\n", 11, - BaseFont.TIMES); + paragraph.addMarkup("And again, the Indentation holds for the rest of the paragraph, or any new indent comes.\nLabels can be aligned:\n", 11, BaseFont.TIMES); + paragraph.addIndent("Left", 100, SpaceUnit.pt, 11, BaseFont.TIMES, Alignment.LEFT); + //PDType1Font.TIMES_BOLD, Alignment.LEFT)); + paragraph.addMarkup("Indent with label aligned to the left.\n", 11, BaseFont.TIMES); + paragraph.addIndent("Center", 100, SpaceUnit.pt, 11, BaseFont.TIMES, Alignment.CENTER); + //PDType1Font.TIMES_BOLD, Alignment.CENTER)); + paragraph.addMarkup("Indent with label aligned to the center.\n", 11, BaseFont.TIMES); + paragraph.addIndent("Right", 100, SpaceUnit.pt, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addMarkup("Indent with label aligned to the right.\n", 11, BaseFont.TIMES); document.add(paragraph); paragraph = new Paragraph(); - paragraph.addMarkup( - "So, what can you do with that? How about lists:\n", 11, - BaseFont.TIMES); - paragraph.add(new Indent(bulletOdd, 4, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addMarkup("So, what can you do with that? How about lists:\n", 11, BaseFont.TIMES); + paragraph.addIndent(bulletOdd, 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("This is a list item\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(bulletOdd, 4, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(bulletOdd, 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("Another list item\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(bulletEven, 8, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(bulletEven, 8, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("Sub list item\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(bulletOdd, 4, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(bulletOdd, 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("And yet another one\n", 11, BaseFont.TIMES); document.add(paragraph); paragraph = new Paragraph(); - paragraph.addMarkup("Also available with indents: Enumerators:\n", 11, - BaseFont.TIMES); + paragraph.addMarkup("Also available with indents: Enumerators:\n", 11, BaseFont.TIMES); RomanEnumerator e1 = new RomanEnumerator(); LowerCaseAlphabeticEnumerator e2 = new LowerCaseAlphabeticEnumerator(); - paragraph.add(new Indent(e1.next() + ". ", 4, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(e1.next() + ". ", 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("First item\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(e1.next() + ". ", 4, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(e1.next() + ". ", 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("Second item\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(e2.next() + ") ", 8, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(e2.next() + ") ", 8, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("A sub item\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(e2.next() + ") ", 8, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(e2.next() + ") ", 8, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("Another sub item\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(e1.next() + ". ", 4, SpaceUnit.em, 11, - PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(e1.next() + ". ", 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("Third item\n", 11, BaseFont.TIMES); document.add(paragraph); paragraph = new Paragraph(); paragraph.addMarkup("The following types are built in:\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(new ArabicEnumerator().next() + " ", 4, - SpaceUnit.em, 11, PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(new ArabicEnumerator().next() + " ", 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("ArabicEnumerator\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(new RomanEnumerator().next() + " ", 4, - SpaceUnit.em, 11, PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(new RomanEnumerator().next() + " ", 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("RomanEnumerator\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(new LowerCaseRomanEnumerator().next() + " ", - 4, SpaceUnit.em, 11, PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(new LowerCaseRomanEnumerator().next() + " ", 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("LowerCaseRomanEnumerator\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(new AlphabeticEnumerator().next() + " ", 4, - SpaceUnit.em, 11, PDType1Font.TIMES_BOLD, Alignment.RIGHT)); + paragraph.addIndent(new AlphabeticEnumerator().next() + " ", 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("AlphabeticEnumerator\n", 11, BaseFont.TIMES); - paragraph.add(new Indent(new LowerCaseAlphabeticEnumerator().next() - + " ", 4, SpaceUnit.em, 11, PDType1Font.TIMES_BOLD, - Alignment.RIGHT)); + paragraph.addIndent(new LowerCaseAlphabeticEnumerator().next() + " ", 4, SpaceUnit.em, 11, BaseFont.TIMES, Alignment.RIGHT); + //PDType1Font.TIMES_BOLD, Alignment.RIGHT)); paragraph.addMarkup("LowerCaseAlphabeticEnumerator\n", 11, BaseFont.TIMES); document.add(paragraph); @@ -149,4 +133,14 @@ public class IndentationTest { document.save(outputStream); } + 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"; } diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Listener.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Listener.java index df93d96..5311b29 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Listener.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/Listener.java @@ -5,6 +5,7 @@ import org.xbib.graphics.pdfbox.layout.elements.Document; import org.xbib.graphics.pdfbox.layout.elements.Paragraph; import org.xbib.graphics.pdfbox.layout.elements.render.RenderContext; import org.xbib.graphics.pdfbox.layout.elements.render.RenderListener; +import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import org.xbib.graphics.pdfbox.layout.text.Alignment; import org.xbib.graphics.pdfbox.layout.font.BaseFont; import org.xbib.graphics.pdfbox.layout.text.Position; @@ -12,7 +13,6 @@ import org.xbib.graphics.pdfbox.layout.text.TextFlow; import org.xbib.graphics.pdfbox.layout.text.TextFlowUtil; import org.xbib.graphics.pdfbox.layout.text.TextSequenceUtil; import java.io.FileOutputStream; -import java.io.IOException; import java.io.OutputStream; public class Listener { @@ -43,15 +43,13 @@ public class Listener { @Override public void beforePage(RenderContext renderContext) { - } @Override - public void afterPage(RenderContext renderContext) - throws IOException { - String content = String.format("Page %s", - renderContext.getPageIndex() + 1); - TextFlow text = TextFlowUtil.createTextFlow(content, 11, BaseFont.HELVETICA); + public void afterPage(RenderContext renderContext) { + String content = String.format("Page %s", renderContext.getPageIndex() + 1); + FontDescriptor fontDescriptor = new FontDescriptor(BaseFont.HELVETICA, 11); + TextFlow text = TextFlowUtil.createTextFlow(content, fontDescriptor); float offset = renderContext.getPageFormat().getMarginLeft() + TextSequenceUtil.getOffset(text, renderContext.getWidth(), Alignment.RIGHT); diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LowLevelText.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LowLevelText.java index 56d61b4..328a543 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LowLevelText.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/LowLevelText.java @@ -5,6 +5,7 @@ import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.junit.jupiter.api.Test; import org.xbib.graphics.pdfbox.layout.elements.PageFormat; +import org.xbib.graphics.pdfbox.layout.font.FontDescriptor; import org.xbib.graphics.pdfbox.layout.shape.RoundRect; import org.xbib.graphics.pdfbox.layout.shape.Shape; import org.xbib.graphics.pdfbox.layout.shape.Stroke; @@ -52,15 +53,12 @@ public class LowLevelText { }); annotationDrawListener.beforePage(null); - TextFlow text = TextFlowUtil - .createTextFlowFromMarkup( + TextFlow text = TextFlowUtil.createTextFlowFromMarkup( "Hello *bold _italic bold-end* italic-end_. Eirmod\ntempor invidunt ut \\*labore", - 11, BaseFont.TIMES); - + new FontDescriptor(BaseFont.TIMES, 11)); text.addText("Spongebob", 11, BaseFont.COURIER); text.addText(" is ", 20, BaseFont.HELVETICA); text.addText("cool", 7, BaseFont.HELVETICA); - text.setMaxWidth(100); float xOffset = TextSequenceUtil.getOffset(text, pageWidth, Alignment.RIGHT); diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MarkupTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MarkupTest.java index feee565..aec68bf 100644 --- a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MarkupTest.java +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/MarkupTest.java @@ -27,31 +27,19 @@ public class MarkupTest { Paragraph paragraph = new Paragraph(); paragraph.addMarkup(text1, 11, BaseFont.TIMES); document.add(paragraph); - paragraph = new Paragraph(); - paragraph - .addMarkup( - "Markup supports *bold*, _italic_, and *even _mixed* markup_.\n\n", - 11, BaseFont.TIMES); - paragraph.addMarkup( - "Escape \\* with \\\\\\* and \\_ with \\\\\\_ in markup.\n\n", + paragraph.addMarkup("Markup supports *bold*, _italic_, and *even _mixed* markup_.\n\n", + 11, BaseFont.TIMES); + paragraph.addMarkup("Escape \\* with \\\\\\* and \\_ with \\\\\\_ in markup.\n\n", + 11, BaseFont.TIMES); + paragraph.addMarkup("And now also {color:#ff0000}c{color:#00ff00}o{color:#0000ff}l{color:#00cccc}o{color:#cc00cc}r{color:#000000}", 11, BaseFont.TIMES); - - paragraph - .addMarkup( - "And now also {color:#ff0000}c{color:#00ff00}o{color:#0000ff}l{color:#00cccc}o{color:#cc00cc}r{color:#000000}", - 11, BaseFont.TIMES); paragraph.addMarkup(" , {_}subscript{_} and {^}superscript{^}.\n\n", 11, BaseFont.TIMES); - - paragraph - .addMarkup( - "You can alternate the position and thickness of an __underline__, " + paragraph.addMarkup("You can alternate the position and thickness of an __underline__, " + "so you may also use this to __{0.25:}strike through__ or blacken __{0.25:20}things__ out\n\n", 11, BaseFont.TIMES); - - document.add(paragraph, new VerticalLayoutHint(Alignment.LEFT, 0, 0, - 30, 0)); + document.add(paragraph, new VerticalLayoutHint(Alignment.LEFT, 0, 0, 30, 0)); paragraph = new Paragraph(); text1 = "\nAlso, you can do all that indentation stuff much easier with markup, e.g. simple indentation\n" diff --git a/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/table/TableTest.java b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/table/TableTest.java new file mode 100644 index 0000000..ab138f0 --- /dev/null +++ b/graphics-pdfbox-layout/src/test/java/org/xbib/graphics/pdfbox/layout/test/table/TableTest.java @@ -0,0 +1,172 @@ +package org.xbib.graphics.pdfbox.layout.test.table; + +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +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.image.PDImageXObject; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.xbib.graphics.pdfbox.layout.font.BaseFont; +import org.xbib.graphics.pdfbox.layout.table.AbstractCell; +import org.xbib.graphics.pdfbox.layout.table.BorderStyle; +import org.xbib.graphics.pdfbox.layout.table.Column; +import org.xbib.graphics.pdfbox.layout.table.HorizontalAlignment; +import org.xbib.graphics.pdfbox.layout.table.ImageCell; +import org.xbib.graphics.pdfbox.layout.table.PdfUtil; +import org.xbib.graphics.pdfbox.layout.table.Row; +import org.xbib.graphics.pdfbox.layout.table.Table; +import org.xbib.graphics.pdfbox.layout.table.TableRenderer; +import org.xbib.graphics.pdfbox.layout.table.TableNotYetBuiltException; +import org.xbib.graphics.pdfbox.layout.table.TextCell; +import java.awt.Color; +import java.nio.file.Files; +import java.nio.file.Paths; + +public class TableTest { + + @Test + public void minimalExample() throws Exception { + try (PDDocument document = new PDDocument()) { + final PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) { + Table myTable = Table.builder() + .addColumnsOfWidth(200, 200) + .padding(2) + .addRow(Row.builder() + .add(TextCell.builder().text("One One").borderWidth(4).backgroundColor(Color.WHITE).build()) + .add(TextCell.builder().text("One Two").borderWidth(0).backgroundColor(Color.YELLOW).build()) + .build()) + .addRow(Row.builder() + .padding(10) + .add(TextCell.builder().text("Two One").textColor(Color.RED).build()) + .add(TextCell.builder().text("Two Two") + .borderWidthRight(1f) + .borderStyleRight(BorderStyle.DOTTED) + .horizontalAlignment(HorizontalAlignment.RIGHT) + .build()) + .build()) + .build(); + TableRenderer tableRenderer = TableRenderer.builder() + .table(myTable) + .contentStream(contentStream) + .startX(20f) + .startY(page.getMediaBox().getUpperRightY() - 20f) + .build(); + tableRenderer.draw(); + } + document.save("build/table-minimal-example.pdf"); + } + } + + @Test + public void getNumberOfColumnsTableBuilderWithThreeColumns() { + final Table.Builder tableBuilder = Table.builder() + .addColumnOfWidth(12) + .addColumnOfWidth(34) + .addColumnOfWidth(56); + final Table table = tableBuilder.build(); + assertThat(table.getNumberOfColumns(), equalTo(3)); + } + + + @Test + public void getWidthTableBuilderWithTwoColumns() { + final Table.Builder tableBuilder = Table.builder() + .addColumnOfWidth(20) + .addColumnOfWidth(40); + final Table table = tableBuilder.build(); + assertThat(table.getWidth(), equalTo(60f)); + } + + + @Test + public void getRowsTableBuilderWithOneRow() { + final Table.Builder tableBuilder = Table.builder(); + tableBuilder.addColumnOfWidth(12) + .addColumnOfWidth(34); + final Row row = Row.builder() + .add(TextCell.builder().text("11").build()) + .add(TextCell.builder().text("12").build()) + .build(); + tableBuilder.addRow(row); + final Table table = tableBuilder.build(); + assertThat(table.getRows().size(), equalTo(1)); + } + + @Test + public void getHeightTwoRowsWithDifferentPaddings() { + final Table table = Table.builder() + .addColumnOfWidth(12) + .addColumnOfWidth(34) + .fontSize(12) + .addRow(Row.builder() + .add(TextCell.builder().text("11").paddingTop(35).paddingBottom(15).build()) + .add(TextCell.builder().text("12").paddingTop(15).paddingBottom(25).build()) + .build()) + .build(); + final float actualFontHeight = PdfUtil.getFontHeight(table.getSettings().getFont(), 12); + assertThat(table.getHeight(), equalTo(50 + actualFontHeight)); + } + + @Test + public void tableBuilderShouldConnectStructureCorrectly() { + AbstractCell lastCell = TextCell.builder().text("").build(); + Table table = Table.builder() + .addColumnOfWidth(10) + .addColumnOfWidth(20) + .addColumnOfWidth(30) + .addRow(Row.builder() + .add(TextCell.builder().text("").colSpan(2).build()) + .add(lastCell) + .build()) + .build(); + Column lastColumn = table.getColumns().get(table.getColumns().size() - 1); + assertThat(lastCell.getColumn(), is(lastColumn)); + } + + @Test + public void getHeightShouldThrowExceptionIfNotYetRendered() { + Assertions.assertThrows(TableNotYetBuiltException.class, () -> { + Row row = Row.builder() + .add(TextCell.builder().text("This text should break because too long").colSpan(2).borderWidth(1).build()) + .add(TextCell.builder().text("Booz").build()) + .wordBreak(true) + .font(BaseFont.COURIER).fontSize(8) + .build(); + row.getHeight(); + }); + } + + @Test + public void getHeightShouldReturnValueIfTableIsBuilt() { + Table.Builder tableBuilder = Table.builder() + .addColumnsOfWidth(10, 10, 10) + .horizontalAlignment(HorizontalAlignment.CENTER) + .fontSize(10).font(BaseFont.HELVETICA) + .wordBreak(false); + Row row = Row.builder() + .add(TextCell.builder().text("iVgebALheQlBkxtDyNDrhKv").colSpan(2).borderWidth(1).build()) + .add(TextCell.builder().text("Booz").build()) + .font(BaseFont.COURIER).fontSize(8) + .build(); + tableBuilder.addRow(row); + tableBuilder.build(); + assertThat(row.getHeight(), greaterThan(0f)); + } + + @Test + public void getHeightShouldThrowExceptionIfTableNotYetBuilt() throws Exception { + Assertions.assertThrows(TableNotYetBuiltException.class, () ->{ + byte[] bytes = Files.readAllBytes(Paths.get("src/test/resources/org/xbib/graphics/pdfbox/layout/test/cat.jpg")); + PDImageXObject image = PDImageXObject.createFromByteArray(new PDDocument(), bytes, "test1"); + AbstractCell cell = ImageCell.builder().image(image).build(); + cell.getHeight(); + }); + } +} diff --git a/graphics-vector-pdf/src/main/java/org/xbib/graphics/io/vector/pdf/util/Resources.java b/graphics-vector-pdf/src/main/java/org/xbib/graphics/io/vector/pdf/util/Resources.java index c4702bb..df2be0a 100644 --- a/graphics-vector-pdf/src/main/java/org/xbib/graphics/io/vector/pdf/util/Resources.java +++ b/graphics-vector-pdf/src/main/java/org/xbib/graphics/io/vector/pdf/util/Resources.java @@ -12,7 +12,6 @@ import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; public class Resources extends PDFObject { @@ -38,11 +37,11 @@ public class Resources extends PDFObject { private final Map transparencies; - private final AtomicInteger currentFontId = new AtomicInteger(); + private int currentFontId; - private final AtomicInteger currentImageId = new AtomicInteger(); + private int currentImageId; - private final AtomicInteger currentTransparencyId = new AtomicInteger(); + private int currentTransparencyId; public Resources(int id, int version) { super(id, version, null, null); @@ -53,13 +52,9 @@ public class Resources extends PDFObject { } private String getResourceId(Map resources, T resource, - String idPrefix, AtomicInteger idCounter) { - String id = resources.get(resource); - if (id == null) { - id = String.format("%s%d", idPrefix, idCounter.getAndIncrement()); - resources.put(resource, id); - } - return id; + String idPrefix, int idCounter) { + return resources.computeIfAbsent(resource, + k -> String.format("%s%d", idPrefix, idCounter)); } @SuppressWarnings("unchecked") @@ -71,7 +66,7 @@ public class Resources extends PDFObject { dict.put(KEY_FONT, dictEntry); } font = getPhysicalFont(font); - String resourceId = getResourceId(fonts, font, PREFIX_FONT, currentFontId); + String resourceId = getResourceId(fonts, font, PREFIX_FONT, currentFontId++); String fontName = font.getPSName(); String fontEncoding = "WinAnsiEncoding"; Map map = new LinkedHashMap<>(); @@ -90,7 +85,7 @@ public class Resources extends PDFObject { dictEntry = new LinkedHashMap<>(); dict.put(KEY_IMAGE, dictEntry); } - String resourceId = getResourceId(images, image, PREFIX_IMAGE, currentImageId); + String resourceId = getResourceId(images, image, PREFIX_IMAGE, currentImageId++); dictEntry.put(resourceId, image); return resourceId; } @@ -103,7 +98,7 @@ public class Resources extends PDFObject { dictEntry = new LinkedHashMap<>(); dict.put(KEY_TRANSPARENCY, dictEntry); } - String resourceId = getResourceId(transparencies, transparency, PREFIX_TRANSPARENCY, currentTransparencyId); + String resourceId = getResourceId(transparencies, transparency, PREFIX_TRANSPARENCY, currentTransparencyId++); Map map = new LinkedHashMap<>(); map.put("Type", "ExtGState"); map.put("ca", transparency);