From dba0f14b8a5f44f627f721d5b57764e031341d29 Mon Sep 17 00:00:00 2001 From: Johannes Merl Date: Fri, 14 Jun 2024 08:28:32 +0200 Subject: [PATCH 1/2] fix build --- src/test/java/be/quodlibet/boxable/DataTableTest.java | 1 - src/test/java/be/quodlibet/boxable/TableTest.java | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/test/java/be/quodlibet/boxable/DataTableTest.java b/src/test/java/be/quodlibet/boxable/DataTableTest.java index 064a3f5..367f8e9 100644 --- a/src/test/java/be/quodlibet/boxable/DataTableTest.java +++ b/src/test/java/be/quodlibet/boxable/DataTableTest.java @@ -2,7 +2,6 @@ import be.quodlibet.boxable.datatable.DataTable; import be.quodlibet.boxable.datatable.UpdateCellProperty; -import com.google.common.io.Files; import java.awt.Color; import java.io.File; import java.io.IOException; diff --git a/src/test/java/be/quodlibet/boxable/TableTest.java b/src/test/java/be/quodlibet/boxable/TableTest.java index c14bc57..dea48bf 100644 --- a/src/test/java/be/quodlibet/boxable/TableTest.java +++ b/src/test/java/be/quodlibet/boxable/TableTest.java @@ -114,7 +114,7 @@ public void Sample1() throws IOException { } } else if(fact[i].equalsIgnoreCase("Google")) { cell = row.createCell((100 / 9f), fact[i]); - cell.setFont(PDType1Font.HELVETICA_OBLIQUE); + cell.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE)); cell.setFontSize(6); cell.setUrl(new URL("https://www.google.de")); } else { @@ -1209,11 +1209,11 @@ public void SampleTest12() throws IOException { // draw page title PageContentStreamOptimized cos = new PageContentStreamOptimized(new PDPageContentStream(doc, page)); - PDStreamUtils.write(cos, "Welcome to your first borderless table", PDType1Font.HELVETICA_BOLD, 14, 15, yStart, + PDStreamUtils.write(cos, "Welcome to your first borderless table", new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 14, 15, yStart, Color.BLACK); cos.close(); - yStart -= FontUtils.getHeight(PDType1Font.HELVETICA_BOLD, 14) + 15; + yStart -= FontUtils.getHeight(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD), 14) + 15; BaseTable table = new BaseTable(yStart, yStartNewPage, bottomMargin, tableWidth, margin, doc, page, drawLines, drawContent); From 5cb6a80e36e525d5df131c14f9a7f8821ed51870 Mon Sep 17 00:00:00 2001 From: Johannes Merl Date: Tue, 25 Jun 2024 10:35:42 +0200 Subject: [PATCH 2/2] big cleanup, add configurable row wrapping --- .../boxable/DefaultRowWrappingFunction.java | 64 ++++++++++ src/main/java/be/quodlibet/boxable/Row.java | 29 +++++ .../boxable/RowWrappingFunction.java | 19 +++ src/main/java/be/quodlibet/boxable/Table.java | 109 ++++++++++-------- .../java/be/quodlibet/boxable/TableTest.java | 70 +++++++++++ 5 files changed, 243 insertions(+), 48 deletions(-) create mode 100644 src/main/java/be/quodlibet/boxable/DefaultRowWrappingFunction.java create mode 100644 src/main/java/be/quodlibet/boxable/RowWrappingFunction.java diff --git a/src/main/java/be/quodlibet/boxable/DefaultRowWrappingFunction.java b/src/main/java/be/quodlibet/boxable/DefaultRowWrappingFunction.java new file mode 100644 index 0000000..636bb70 --- /dev/null +++ b/src/main/java/be/quodlibet/boxable/DefaultRowWrappingFunction.java @@ -0,0 +1,64 @@ +package be.quodlibet.boxable; + +import org.apache.pdfbox.pdmodel.PDPage; + +import java.util.Arrays; +import java.util.List; + +/** + * This Wrapping function allows you to create advanced table layouts by hiding borders, + * without braking them on pagebreaks. e.g.: + * ┌────┬──────┬──────┐ + * ├────┼──────┤      │ + * ├────┼──────┤      │ + * ├────┼──────┤      │ + * ├────┼──────┤      │ + * ├────┼──────┤      │ + * └────┴──────┴──────┘ + *

+ * is not broken between pages + */ +public class DefaultRowWrappingFunction implements RowWrappingFunction { + + + @Override + public int[] getWrappableRows(List> rows) { + int[] wrapableRows = new int[rows.size()]; + wrapableRows[0] = 0; + int j = 1; + for (int i = 1; i < rows.size(); i++) { + Row row = rows.get(i); + if (isProperTableRowStart(row)) { + wrapableRows[j++] = i; + } + } + return Arrays.copyOf(wrapableRows, j); + } + + /* + * Checks if this is a proper start of a table row, + * i.e. every cell is visibly closed by a border on top or has no sides anyway. + * + * allowed: + * "┌────┐", " " + * not allowed: + * "│ │", "│ "," │" + * */ + private boolean isProperTableRowStart(Row row) { + if (row.getCells().isEmpty()) { + return true; + } + boolean previousClosed = false; + for (Cell cell : row.getCells()) { + + if (cell.getTopBorder() == null && (cell.getLeftBorder() != null && !previousClosed)) { + return false; + } + previousClosed = cell.getTopBorder() != null; + } + Cell lastCell = row.getCells().get(row.getCells().size() - 1); + + return lastCell.getTopBorder() != null || (lastCell.getRightBorder() == null); + } + +} diff --git a/src/main/java/be/quodlibet/boxable/Row.java b/src/main/java/be/quodlibet/boxable/Row.java index b5fb3e4..fae5fea 100644 --- a/src/main/java/be/quodlibet/boxable/Row.java +++ b/src/main/java/be/quodlibet/boxable/Row.java @@ -22,6 +22,7 @@ public class Row { private boolean headerRow = false; float height; private float lineSpacing = 1; + private float wrapHeight = -1; Row(Table table, List> cells, float height) { this.table = table; @@ -285,4 +286,32 @@ public float getLineSpacing() { public void setLineSpacing(float lineSpacing) { this.lineSpacing = lineSpacing; } + + + /** + * Finds out, taking restricted row wrapping into account, how much vertical space this row, + * together with all un-wrappable rows that follow, will take. + * + * @return wrapHeight + */ + public float getWrapHeight() { + return table.calcWrapHeight(this); + } + + protected void setWrapHeight(float wrapHeight) { + this.wrapHeight = wrapHeight; + } + + protected float getSavedWrapHeight() { + return this.wrapHeight; + } + + public boolean hasBottomBorder() { + for (Cell cell : cells) { + if (cell.getBottomBorder() == null) { + return false; + } + } + return !cells.isEmpty(); + } } diff --git a/src/main/java/be/quodlibet/boxable/RowWrappingFunction.java b/src/main/java/be/quodlibet/boxable/RowWrappingFunction.java new file mode 100644 index 0000000..0a292ba --- /dev/null +++ b/src/main/java/be/quodlibet/boxable/RowWrappingFunction.java @@ -0,0 +1,19 @@ +package be.quodlibet.boxable; + +import org.apache.pdfbox.pdmodel.PDPage; + +import java.util.List; + +/** + * This interface allows you to specify where in the table page breaks may be inserted. + */ +public interface RowWrappingFunction { + + + /** + * Allows you to specify at which indexes rows that can be moved to the next page are. + * @param rows list of rows to consider + * @return indexes of rows that may be moved to the next page, if the current page is full + */ + int[] getWrappableRows(List> rows); +} diff --git a/src/main/java/be/quodlibet/boxable/Table.java b/src/main/java/be/quodlibet/boxable/Table.java index 28a0fe5..3ebe88c 100644 --- a/src/main/java/be/quodlibet/boxable/Table.java +++ b/src/main/java/be/quodlibet/boxable/Table.java @@ -1,4 +1,3 @@ - /* Quodlibet.be */ @@ -56,6 +55,8 @@ public abstract class Table { private PageProvider pageProvider; + private RowWrappingFunction rowWrappingFunction = new DefaultRowWrappingFunction(); + // page margins private final float pageTopMargin; private final float pageBottomMargin; @@ -83,7 +84,7 @@ public Table(float yStart, float yStartNewPage, float pageBottomMargin, float wi this(yStart, yStartNewPage, 0, pageBottomMargin, width, margin, document, currentPage, drawLines, drawContent, null); } - + /** * @deprecated Use one of the constructors that pass a {@link PageProvider} * @param yStartNewPage Y position where possible new page of {@link Table} @@ -223,11 +224,16 @@ public Row createRow(List> cells, float height) { public float draw() throws IOException { ensureStreamIsOpen(); + calcWrapHeightsAndRowHeights(); + + // for the first row in the table, we have to draw the top border + removeTopBorders = false; + 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 not draw them on another page if (isEndOfPage(getMinimumHeight())) { pageBreak(); tableStartedAtNewPage = true; @@ -241,17 +247,9 @@ public float draw() throws IOException { } 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(); - } - } + // row.getHeight is currently an extremely expensive function, so we get the value + // calculated in calcWrapHeightsAndRowHeights, since it will not change during drawing + float rowHeight = row.getLineHeight(); // draw the bookmark if (row.getBookmark() != null) { @@ -262,15 +260,12 @@ private void drawRow(Row row) throws IOException { 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)) { + if (isEndOfPage(row.getSavedWrapHeight()) && !header.contains(row)) { // Draw line at bottom of table endTable(); @@ -278,33 +273,15 @@ private void drawRow(Row row) throws IOException { // 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)) { + // after a page break, we have to ensure that top borders get + // drawn removeTopBorders = false; - } - if (removeTopBorders) { - row.removeTopBorders(); - } + // redraw all headers on each page + for (Row headerRow : header) { + drawRow(headerRow); + } - // 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) { @@ -318,6 +295,9 @@ private void drawRow(Row row) throws IOException { if (drawContent) { drawCellContent(row, rowHeight); } + //would be better to check line presence between rows and + //draw exactly what's needed between two rows in one go. + removeTopBorders = row.hasBottomBorder(); } /** @@ -398,9 +378,9 @@ private void drawCellContent(Row row, float rowHeight) throws IOException { break; } imageCell.getImage().draw(document, tableContentStream, cursorX, cursorY); - + if (imageCell.getUrl() != null) { - List annotations = ((PDPage)currentPage).getAnnotations(); + List annotations = ((PDPage) currentPage).getAnnotations(); PDBorderStyleDictionary borderULine = new PDBorderStyleDictionary(); borderULine.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE); @@ -411,7 +391,7 @@ private void drawCellContent(Row row, float rowHeight) throws IOException { // Set the rectangle containing the link // PDRectangle sets a the x,y and the width and height extend upwards from that! - PDRectangle position = new PDRectangle(cursorX, cursorY, (float)(imageCell.getImage().getWidth()), -(float)(imageCell.getImage().getHeight())); + PDRectangle position = new PDRectangle(cursorX, cursorY, (float) (imageCell.getImage().getWidth()), -(float) (imageCell.getImage().getHeight())); txtLink.setRectangle(position); // add an action @@ -564,14 +544,14 @@ private void drawCellContent(Row row, float rowHeight) throws IOException { cursorY -= cell.getVerticalFreeSpace(); break; } - + if (cell.getUrl() != null) { - List annotations = ((PDPage)currentPage).getAnnotations(); + List annotations = ((PDPage) currentPage).getAnnotations(); PDAnnotationLink txtLink = new PDAnnotationLink(); // Set the rectangle containing the link // PDRectangle sets a the x,y and the width and height extend upwards from that! - PDRectangle position = new PDRectangle(cursorX - 5, cursorY + 10, (float)(cell.getWidth()), -(float)(cell.getHeight())); + PDRectangle position = new PDRectangle(cursorX - 5, cursorY + 10, (float) (cell.getWidth()), -(float) (cell.getHeight())); txtLink.setRectangle(position); // add an action @@ -938,6 +918,10 @@ protected void setYStart(float yStart) { this.yStart = yStart; } + public void setRowWrappingFunction(RowWrappingFunction rowWrappingFunction) { + this.rowWrappingFunction = rowWrappingFunction; + } + public boolean isDrawDebug() { return drawDebug; } @@ -978,4 +962,33 @@ public void removeAllBorders(boolean removeAllBorders) { this.removeAllBorders = removeAllBorders; } + protected float calcWrapHeight(Row tRow) { + int[] wrappableRows = rowWrappingFunction.getWrappableRows(rows); + int currentRow = rows.indexOf(tRow); + for (int wrappableRow : wrappableRows) { + if (wrappableRow > currentRow) { + float wrapHeight = 0; + for (int i = currentRow; i < wrappableRow; i++) { + wrapHeight += rows.get(i).getHeight(); + } + return wrapHeight; + } + } + + return tRow.getHeight(); + } + + protected void calcWrapHeightsAndRowHeights() { + int[] wrappableRows = rowWrappingFunction.getWrappableRows(rows); + int prevRow = rows.size(); + for (int i = wrappableRows.length - 1; i >= 0; i--) { + int wrappableRow = wrappableRows[i]; + float height = 0; + for (int j = prevRow - 1; j >= wrappableRow; j--) { + height += rows.get(j).getHeight(); + rows.get(j).setWrapHeight(height); + } + prevRow = wrappableRow; + } + } } diff --git a/src/test/java/be/quodlibet/boxable/TableTest.java b/src/test/java/be/quodlibet/boxable/TableTest.java index dea48bf..fb34f6b 100644 --- a/src/test/java/be/quodlibet/boxable/TableTest.java +++ b/src/test/java/be/quodlibet/boxable/TableTest.java @@ -1256,6 +1256,76 @@ public void onContentDrawn(Cell cell, PDDocument document, PDPage page, assertTrue(callbackCalled[0]); } + /** + *

+ * Test for a table using the following features : + *

+ *

+ * + * @throws IOException + */ + @Test + public void SampleTest13() throws IOException { + // Set margins + float margin = 10; + + // Initialize Document + PDDocument doc = new PDDocument(); + PDPage page = addNewPage(doc); + + // Initialize table + float tableWidth = page.getMediaBox().getWidth() - (2 * margin); + float yStartNewPage = page.getMediaBox().getHeight() - (2 * margin); + boolean drawContent = true; + boolean drawLines = true; + float yStart = yStartNewPage; + float pageBottomMargin = 70; + float pageTopMargin = 2*margin; + BaseTable table = new BaseTable(yStart, yStartNewPage, pageBottomMargin, tableWidth, margin, doc, page, drawLines, + drawContent); + + // set default line spacing for entire table + table.setLineSpacing(1.5f); + + //ten unwrappable blocks + for (int i = 0; i < 10; i++) { + // first row in unwrappabe block + Row row = table.createRow(10f); + row.createCell(40,"first test"); + row.createCell(30,"first test"); + Cell lastCell = row.createCell(30, "first test"); + lastCell.setBottomBorderStyle(null); + + // 13 more rows + for (int j = 0; j < 13; j++) { + row = table.createRow(10f); + row.createCell(40,"test"); + row.createCell(30,"test"); + lastCell = row.createCell(30, "test"); + lastCell.setBottomBorderStyle(null); + lastCell.setTopBorderStyle(null); + } + // last row in unwrappabe block + row = table.createRow(10f); + row.createCell(40,"test"); + row.createCell(30,"test"); + lastCell = row.createCell(30, "test"); + lastCell.setTopBorderStyle(null); + } + table.draw(); + + // Save the document + File file = new File("target/BoxableSample13.pdf"); + System.out.println("Sample file saved at : " + file.getAbsolutePath()); + file.getParentFile().mkdirs(); + doc.save(file); + doc.close(); + + + } + private static PDPage addNewPage(PDDocument doc) { PDPage page = new PDPage(); doc.addPage(page);