diff --git a/bom/pom.xml b/bom/pom.xml index b99f8473569..461af0e14a6 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -164,8 +164,7 @@ identified 3.4.5 - 3.0.0 - 1.7.0 + 3.0.4 2.15 5.4.0 @@ -1891,25 +1890,6 @@ identified - - com.github.dhorions - boxable - ${boxable.version} - - - org.apache.commons - commons-csv - - - org.slf4j - slf4j-api - - - org.jsoup - jsoup - - - org.apache.poi diff --git a/extensions/vw/tabular/pdf/pom.xml b/extensions/vw/tabular/pdf/pom.xml index 373e49014a5..ce95e20ea99 100644 --- a/extensions/vw/tabular/pdf/pom.xml +++ b/extensions/vw/tabular/pdf/pom.xml @@ -47,10 +47,6 @@ org.slf4j slf4j-api - - org.jsoup - jsoup - @@ -64,11 +60,6 @@ pdfbox - - com.github.dhorions - boxable - - diff --git a/extensions/vw/tabular/pdf/src/main/java/module-info.java b/extensions/vw/tabular/pdf/src/main/java/module-info.java new file mode 100644 index 00000000000..f4254154e0d --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/module-info.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +module org.apache.causeway.extensions.tabular.pdf { + exports org.apache.causeway.extensions.tabular.pdf.factory; + exports org.apache.causeway.extensions.tabular.pdf.exporter; + exports org.apache.causeway.extensions.tabular.pdf; + + requires static lombok; + + requires java.desktop; + + requires org.apache.causeway.applib; + requires org.apache.causeway.commons; + requires org.apache.logging.log4j; + requires org.apache.pdfbox; + requires org.apache.pdfbox.io; + requires org.jsoup; + requires spring.context; + requires spring.core; +} \ No newline at end of file diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/CellFactory.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/CellFactory.java index 22c93b1d60b..b75b579eb78 100644 --- a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/CellFactory.java +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/CellFactory.java @@ -29,47 +29,48 @@ import java.util.Date; import java.util.List; -import be.quodlibet.boxable.Cell; -import be.quodlibet.boxable.Row; -import be.quodlibet.boxable.image.Image; +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Cell; +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Row; -record CellFactory(Row row, Cell template) { +record CellFactory(Row row, Cell template) { + + public Cell createCell(final int i, final float width, final List rowData) { - public Cell createCell(int i, float width, List rowData) { - Object cellValue = null; if (rowData.size() >= i) { cellValue = rowData.get(i); - if (cellValue instanceof String s) { - cellValue = s.replaceAll("\n", "
"); + if (cellValue instanceof CharSequence seq) { + cellValue = seq.toString() + .replaceAll("\r", "") + .replaceAll("\n", "
"); } } if(cellValue==null) cellValue = ""; - + var cell = switch(cellValue.getClass().getSimpleName()) { - case "BufferedImage" -> row.createImageCell(width, new Image((BufferedImage)cellValue)); + case "BufferedImage" -> row.createImageCell(width, (BufferedImage)cellValue); default -> row.createCell(width, toString(cellValue)); }; - + cell.copyCellStyle(template); - + return cell; } /** * @param cellValue not {@code null} nor {@link BufferedImage} */ - private String toString(Object valueAsObj) { + private String toString(final Object valueAsObj) { // String - if(valueAsObj instanceof CharSequence value) { - return value.toString(); + if(valueAsObj instanceof String value) { + return value; } // boolean if(valueAsObj instanceof Boolean value) { - return value ? "✔" : "━"; + return value ? "+" : "-"; } - + // date if(valueAsObj instanceof Date value) { var dateTime = LocalDateTime.ofInstant(value.toInstant(), ZoneId.systemDefault()); @@ -114,5 +115,5 @@ private String toString(Object valueAsObj) { // if all else fails fallback to value's toString method return valueAsObj.toString(); } - + } diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/FontFactory.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/FontFactory.java new file mode 100644 index 00000000000..82596268f53 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/FontFactory.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory; + +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName; + +import lombok.experimental.UtilityClass; + +@UtilityClass +class FontFactory { + + PDFont helvetica() { + return new PDType1Font(FontName.HELVETICA); + } + + PDFont helveticaBold() { + return new PDType1Font(FontName.HELVETICA_BOLD); + } + +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/FontUtils.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/FontUtils.java new file mode 100644 index 00000000000..f6be34890ec --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/FontUtils.java @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; + +import lombok.experimental.UtilityClass; +import lombok.extern.log4j.Log4j2; + +@UtilityClass +@Log4j2 +public class FontUtils { + + private record FontMetrics( + float height, + float ascent, + float descent) { + } + + /** + * {@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<>(); + + /** + * 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 final PDType0Font loadFont(final PDDocument document, final String fontPath) { + try { + return PDType0Font.load(document, FontUtils.class.getClassLoader().getResourceAsStream(fontPath)); + } catch (IOException e) { + log.warn("Cannot load given external font", e); + return null; + } + } + + /** + * 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) { + // turn into runtime exception + throw new IllegalStateException("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).ascent * 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).descent * 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).height * fontSize; + } + + /** + * Create basic {@link FontMetrics} for current font. + * @param font + * The font from which calculation will be applied <<<<<<< HEAD + * @throws IOException + * If reading the font file fails ======= >>>>>>> using FreeSans + * as default font and added new free fonts + */ + 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 void addDefaultFonts(final PDFont font, final PDFont fontBold, final PDFont fontItalic, + final PDFont fontBoldItalic) { + defaultFonts.put("font", font); + defaultFonts.put("fontBold", fontBold); + defaultFonts.put("fontItalic", fontItalic); + defaultFonts.put("fontBoldItalic", fontBoldItalic); + } + + public static Map getDefaultfonts() { + return defaultFonts; + } + + public static void setSansFontsAsDefault(final PDDocument document) { + defaultFonts.put("font", loadFont(document, "fonts/FreeSans.ttf")); + defaultFonts.put("fontBold", loadFont(document, "fonts/FreeSansBold.ttf")); + defaultFonts.put("fontItalic", loadFont(document, "fonts/FreeSansOblique.ttf")); + defaultFonts.put("fontBoldItalic", loadFont(document, "fonts/FreeSansBoldOblique.ttf")); + } +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/HorizontalAlignment.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/HorizontalAlignment.java new file mode 100644 index 00000000000..1b03003c626 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/HorizontalAlignment.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory; + +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/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/LineStyle.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/LineStyle.java new file mode 100644 index 00000000000..051f534e4d5 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/LineStyle.java @@ -0,0 +1,138 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory; + +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(final 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; + } + if (Float.floatToIntBits(this.width) != Float.floatToIntBits(other.width)) { + return false; + } + return true; + } + + +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/PdfFactory.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/PdfFactory.java index d194ae4a6cb..3d2d5e56a08 100644 --- a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/PdfFactory.java +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/PdfFactory.java @@ -27,20 +27,17 @@ import org.apache.pdfbox.pdmodel.PDPageContentStream; import org.apache.pdfbox.pdmodel.common.PDRectangle; import org.apache.pdfbox.pdmodel.font.PDFont; -import org.apache.pdfbox.pdmodel.font.PDType1Font; -import org.apache.pdfbox.pdmodel.font.Standard14Fonts.FontName; import org.apache.causeway.applib.value.Blob; import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType; import org.apache.causeway.commons.io.DataSink; +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Table; import lombok.Builder; import lombok.SneakyThrows; -import be.quodlibet.boxable.BaseTable; - public class PdfFactory implements AutoCloseable { - + @Builder public record Options( PDRectangle pdRectangle) { @@ -53,7 +50,7 @@ public static OptionsBuilder a4Landscape() { .pdRectangle(new PDRectangle(PDRectangle.A4.getHeight(), PDRectangle.A4.getWidth())); } } - + final Options options; final PDDocument document; final PDPage page; @@ -62,10 +59,10 @@ public static OptionsBuilder a4Landscape() { final float tablesmargin; final float tableWidth; final float yStartNewPage; - + float yStart; - - public PdfFactory(Options options) { + + public PdfFactory(final Options options) { this.options = options; this.document = new PDDocument(); this.page = new PDPage(); @@ -78,19 +75,19 @@ public PdfFactory(Options options) { this.yStartNewPage = page.getMediaBox().getHeight() - (2 * margin); this.yStart = yStartNewPage; } - - public PDDocument document() { return document; } - + + public PDDocument document() { return document; } + @SneakyThrows @Override public void close() { document.close(); } - + @SneakyThrows - public void drawHeader(String text) { + public void drawHeader(final String text) { try (PDPageContentStream contents = new PDPageContentStream(document, page)) { - PDFont font = new PDType1Font(FontName.HELVETICA_BOLD); + PDFont font = FontFactory.helveticaBold(); contents.beginText(); contents.setFont(font, 12); contents.newLineAtOffset(margin, yStart); @@ -99,32 +96,31 @@ public void drawHeader(String text) { this.yStart-= 16; //TODO calculate from line height } } - + @SneakyThrows public void drawTable( - List colWidths, - List primaryHeaderTexts, - List secondaryHeaderTexts, - List> rowData) { - var baseTable = new BaseTable(yStart, yStartNewPage, bottomMargin, tableWidth, margin, - document, page, true, true); + final List colWidths, + final List primaryHeaderTexts, + final List secondaryHeaderTexts, + final List> rowData) { + var baseTable = Table.create(yStart, yStartNewPage, 0, bottomMargin, tableWidth, margin, document, page); var pdfTable = new PdfTable(baseTable, page, colWidths, primaryHeaderTexts, secondaryHeaderTexts); pdfTable.appendRows(rowData); this.yStart = baseTable.draw() - tablesmargin; } - + @SneakyThrows - public void writeToFile(File file) { + public void writeToFile(final File file) { document.save(file); } - - public Blob toBlob(String name) { + + public Blob toBlob(final String name) { var result = new AtomicReference(); - + DataSink.ofByteArrayConsumer(bytes-> result.set(Blob.of(name, CommonMimeType.PDF, bytes))) .writeAll(document::save); - + return result.get(); } diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/PdfTable.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/PdfTable.java index 24a313f132b..4f343a357e8 100644 --- a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/PdfTable.java +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/PdfTable.java @@ -26,39 +26,30 @@ import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; -import org.apache.pdfbox.pdmodel.font.PDType1Font; -import org.apache.pdfbox.pdmodel.font.Standard14Fonts; import org.apache.causeway.commons.internal.base._NullSafe; +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Cell; +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Row; +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Table; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; -import be.quodlibet.boxable.BaseTable; -import be.quodlibet.boxable.Cell; -import be.quodlibet.boxable.HorizontalAlignment; -import be.quodlibet.boxable.Row; -import be.quodlibet.boxable.Table; -import be.quodlibet.boxable.VerticalAlignment; -import be.quodlibet.boxable.line.LineStyle; -import be.quodlibet.boxable.utils.FontUtils; - -@SuppressWarnings("rawtypes") -class PdfTable { - +final class PdfTable { + @Getter @Setter private Table table; private final List primaryHeaderTexts; private final List secondaryHeaderTexts; @Getter @Setter private List colWidths; - + private final Cell primaryHeaderTemplate; private final Cell secondaryHeaderTemplate; private final Cell evenTemplate; private final Cell oddTemplate; @SneakyThrows - PdfTable(Table table, PDPage page, List colWidths, List primaryHeaderTexts, List secondaryHeaderTexts) { + PdfTable(final Table table, final PDPage page, final List colWidths, final List primaryHeaderTexts, final List secondaryHeaderTexts) { this.table = table; this.primaryHeaderTexts = primaryHeaderTexts; this.secondaryHeaderTexts = secondaryHeaderTexts; @@ -69,28 +60,27 @@ class PdfTable { dpage.setMediaBox(page.getMediaBox()); dpage.setRotation(page.getRotation()); ddoc.addPage(dpage); - BaseTable dummyTable = new BaseTable(10f, 10f, 10f, table.getWidth(), 10f, ddoc, dpage, false, false); + var dummyTable = Table.dummy(10f, 10f, 0, 10f, table.getWidth(), 10f, ddoc, dpage); Row dr = dummyTable.createRow(0f); this.primaryHeaderTemplate = dr.createCell(10f, "A", HorizontalAlignment.CENTER, VerticalAlignment.MIDDLE); this.secondaryHeaderTemplate = dr.createCell(10f, "A", HorizontalAlignment.LEFT, VerticalAlignment.MIDDLE); this.evenTemplate = dr.createCell(10f, "A", HorizontalAlignment.CENTER, VerticalAlignment.MIDDLE); this.oddTemplate = dr.createCell(10f, "A", HorizontalAlignment.CENTER, VerticalAlignment.MIDDLE); - + setDefaultStyles(); ddoc.close(); } - @SuppressWarnings("unchecked") - void appendRows(List> rows) throws IOException { + void appendRows(final List> rows) throws IOException { Map colWidths = new HashMap<>(); int numcols = 0; - + { // header // calculate the width of the columns float totalWidth = 0.0f; if (this.colWidths == null) { List rowData = this.primaryHeaderTexts; - + for (int i = 0; i < rowData.size(); i++) { String cellValue = rowData.get(i); float textWidth = FontUtils.getStringWidth(primaryHeaderTemplate.getFont(), " " + cellValue + " ", @@ -128,7 +118,7 @@ void appendRows(List> rows) throws IOException { numcols = i; } } - + // add primary header row { List rowData = this.primaryHeaderTexts; @@ -142,7 +132,7 @@ void appendRows(List> rows) throws IOException { } table.addHeaderRow(h); } - + // add secondary header row if(!_NullSafe.isEmpty(this.secondaryHeaderTexts)) { List rowData = this.secondaryHeaderTexts; @@ -157,18 +147,18 @@ void appendRows(List> rows) throws IOException { table.addHeaderRow(h); } } - + int rowIndex = 0; for (List rowData : rows) { - - final Cell template = rowIndex%2 == 0 + + final Cell template = rowIndex%2 == 0 ? evenTemplate : oddTemplate; - + var row = table.createRow(template.getCellHeight()); - + var cellFactory = new CellFactory(row, template); - + for (int i = 0; i <= numcols; i++) { cellFactory.createCell(i, colWidths.get(i), rowData); } @@ -183,24 +173,24 @@ private void setDefaultStyles() { primaryHeaderTemplate.setFillColor(new Color(137, 218, 245)); primaryHeaderTemplate.setTextColor(Color.BLACK); - primaryHeaderTemplate.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD)); + primaryHeaderTemplate.setFont(FontFactory.helveticaBold()); primaryHeaderTemplate.setBorderStyle(thinline); - + secondaryHeaderTemplate.setFillColor(Color.LIGHT_GRAY); secondaryHeaderTemplate.setTextColor(Color.BLACK); - secondaryHeaderTemplate.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA)); + secondaryHeaderTemplate.setFont(FontFactory.helvetica()); secondaryHeaderTemplate.setFontSize(7); secondaryHeaderTemplate.setBorderStyle(thinline); - + evenTemplate.setFillColor(new Color(242, 242, 242)); evenTemplate.setTextColor(Color.BLACK); - evenTemplate.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA)); + evenTemplate.setFont(FontFactory.helvetica()); evenTemplate.setBorderStyle(thinline); - + oddTemplate.setFillColor(new Color(230, 230, 230)); oddTemplate.setTextColor(Color.BLACK); - oddTemplate.setFont(new PDType1Font(Standard14Fonts.FontName.HELVETICA)); + oddTemplate.setFont(FontFactory.helvetica()); oddTemplate.setBorderStyle(thinline); } - + } diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/VerticalAlignment.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/VerticalAlignment.java new file mode 100644 index 00000000000..57103aa71fe --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/VerticalAlignment.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory; + +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/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Cell.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Cell.java new file mode 100644 index 00000000000..5eb6b632a1c --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Cell.java @@ -0,0 +1,437 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import java.awt.Color; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; + +import org.apache.causeway.extensions.tabular.pdf.factory.FontUtils; +import org.apache.causeway.extensions.tabular.pdf.factory.HorizontalAlignment; +import org.apache.causeway.extensions.tabular.pdf.factory.LineStyle; +import org.apache.causeway.extensions.tabular.pdf.factory.VerticalAlignment; + +import lombok.Getter; +import lombok.Setter; + +public class Cell { + + @Getter private float width; + /** + * the height of the single cell. + */ + @Setter private Float height; + /** cell's text value */ + @Getter private String text; + + @Getter @Setter private URL url = null; + + private PDFont font = new PDType1Font(Standard14Fonts.FontName.HELVETICA); + + /** + * The {@linkplain PDFont font} used for bold text, for example in + * {@linkplain #isHeaderCell() header cells}. + */ + @Getter @Setter private PDFont fontBold = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); + + /** {@link PDFont} size for current cell (in points) */ + @Getter private float fontSize = 8; + @Getter @Setter private Color fillColor; + /** + * {@link Color} of the cell's text + */ + @Getter @Setter private Color textColor = Color.BLACK; + private final Row row; + private WrappingFunction wrappingFunction; + @Getter @Setter private boolean headerCell = false; + @Getter @Setter private boolean colspanCell = false; + + // default padding + /** cell's left padding (in points) */ + @Getter @Setter private float leftPadding = 5f; + /** cell's right padding (in points) */ + @Getter @Setter private float rightPadding = 5f; + /** cell's top padding (in points) */ + @Getter @Setter private float topPadding = 5f; + /** cell's bottom padding (in points) */ + @Getter @Setter private float bottomPadding = 5f; + + // default border + @Getter @Setter private LineStyle leftBorderStyle = new LineStyle(Color.BLACK, 1); + @Getter @Setter private LineStyle rightBorderStyle = new LineStyle(Color.BLACK, 1); + @Getter @Setter private LineStyle topBorderStyle = new LineStyle(Color.BLACK, 1); + @Getter @Setter private LineStyle bottomBorderStyle = new LineStyle(Color.BLACK, 1); + + private Paragraph paragraph = null; + @Getter @Setter private float lineSpacing = 1; + @Getter @Setter private boolean textRotated = false; + + @Getter @Setter private HorizontalAlignment align; + @Getter @Setter private VerticalAlignment valign; + + float horizontalFreeSpace = 0; + float verticalFreeSpace = 0; + + private final List contentDrawnListenerList = new ArrayList<>(); + + /** + * Constructs a cell with the default alignment + * {@link VerticalAlignment#TOP} {@link HorizontalAlignment#LEFT}. + * @see Cell#Cell(Row, float, String, boolean, HorizontalAlignment, + * VerticalAlignment) + */ + Cell(final Row row, final float width, final String text, final 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(final Row row, final float width, final String text, final boolean isCalculated, final HorizontalAlignment align, + final VerticalAlignment valign) { + this.row = row; + if (isCalculated) { + double calculatedWidth = row.getWidth() * (width / 100); + this.width = (float) calculatedWidth; + } 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; + } + + /** + * Returns cell's width without (left,right) padding. + */ + public float getInnerWidth() { + return getWidth() - getLeftPadding() - getRightPadding() + - (leftBorderStyle == null ? 0 : leftBorderStyle.getWidth()) + - (rightBorderStyle == null ? 0 : rightBorderStyle.getWidth()); + } + + /** + * Returns cell's height without (top,bottom) padding. + */ + public float getInnerHeight() { + return getHeight() - getBottomPadding() - getTopPadding() + - (topBorderStyle == null ? 0 : topBorderStyle.getWidth()) + - (bottomBorderStyle == null ? 0 : bottomBorderStyle.getWidth()); + } + + /** + * Sets cell's text value + * @param text + * Text value of the cell + */ + public void setText(final String text) { + this.text = text; + + // paragraph invalidated + paragraph = null; + } + + /** + * Returns appropriate {@link PDFont} for current cell. + * @throws IllegalArgumentException + * if font is not set. + */ + public PDFont getFont() { + if (font == null) throw new IllegalArgumentException("Font not set."); + return isHeaderCell() ? fontBold : font; + } + + /** + * Sets appropriate {@link PDFont} for current cell. + * @param font + * {@link PDFont} for current cell + */ + public void setFont(final PDFont font) { + this.font = font; + + // paragraph invalidated + paragraph = null; + } + + /** + * Sets {@link PDFont} size for current cell (in points). + * @param fontSize + * {@link PDFont} size for current cell (in points). + */ + public void setFontSize(final 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(); + } + + /** + * Returns the cell's height according to {@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() + + (getTopBorderStyle() == null ? 0 : getTopBorderStyle().getWidth()) + getBottomPadding() + + (getBottomBorderStyle() == null ? 0 : getBottomBorderStyle().getWidth()); + } catch (final IOException e) { + throw new IllegalStateException("Font not set.", e); + } + } else { + return getTextHeight() + getTopPadding() + getBottomPadding() + + (getTopBorderStyle() == null ? 0 : getTopBorderStyle().getWidth()) + + (getBottomBorderStyle() == null ? 0 : getBottomBorderStyle().getWidth()); + } + } + + /** + * Returns {@link Paragraph}'s height + */ + public float getTextHeight() { + return getParagraph().getHeight(); + } + + /** + * Returns {@link Paragraph}'s width + */ + public float getTextWidth() { + return getParagraph().getWidth(); + } + + /** + * Returns 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}. + */ + public float getVerticalFreeSpace() { + if (isTextRotated()) { + // need to calculate max line width so we just iterating through all lines + getParagraph().getLines().forEach(line->{}); + return getInnerHeight() - getParagraph().getMaxLineWidth(); + } else { + return getInnerHeight() - getTextHeight(); + } + } + + /** + * Returns 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}. + */ + public float getHorizontalFreeSpace() { + if (isTextRotated()) { + return getInnerWidth() - getTextHeight(); + } else { + return getInnerWidth() - getParagraph().getMaxLineWidth(); + } + } + + + public WrappingFunction getWrappingFunction() { + return getParagraph().getWrappingFunction(); + } + + public void setWrappingFunction(final WrappingFunction wrappingFunction) { + this.wrappingFunction = wrappingFunction; + + // paragraph invalidated + paragraph = null; + } + + /** + * Easy setting for cell border style. + * @param border + * It is {@link LineStyle} for all borders + * @see LineStyle Rendering line attributes + */ + public void setBorderStyle(final LineStyle border) { + this.leftBorderStyle = border; + this.rightBorderStyle = border; + this.topBorderStyle = border; + this.bottomBorderStyle = border; + } + + /** + * 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(final Cell sourceCell) { + Boolean leftBorder = this.leftBorderStyle == null; + setBorderStyle(sourceCell.getTopBorderStyle()); + 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()); + } + + /** + * Returns whether source cell has the same style + * @param sourceCell Source {@link Cell} which will be used for style comparison + */ + public Boolean hasSameStyle(final Cell sourceCell) { + if (!sourceCell.getTopBorderStyle().equals(getTopBorderStyle())) { + 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; + } + if (!sourceCell.getValign().equals(getValign())) { + return false; + } + return true; + } + + public void addContentDrawnListener(final CellContentDrawnListener listener) { + contentDrawnListenerList.add(listener); + } + + public List getCellContentDrawnListeners() { + return contentDrawnListenerList; + } + + public void notifyContentDrawnListeners(final PDDocument document, final PDPage page, final PDRectangle rectangle) { + for(var listener : getCellContentDrawnListeners()) { + listener.onContentDrawn(this, document, page, rectangle); + } + } + +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/CellContentDrawnListener.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/CellContentDrawnListener.java new file mode 100644 index 00000000000..c409313d7d0 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/CellContentDrawnListener.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; + +interface CellContentDrawnListener { + void onContentDrawn(Cell cell, PDDocument document, PDPage page, PDRectangle rectangle); +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Image.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Image.java new file mode 100644 index 00000000000..9332285ff76 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Image.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; +import java.awt.image.BufferedImage; +import java.io.IOException; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; + +final class Image { + + private final BufferedImage image; + private float width; + private float height; + private PDImageXObject imageXObject = null; + + // standard DPI + private float[] dpi = { 72, 72 }; + private float quality = 1f; + + /** + * 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, final float dpi) { + this(image, dpi, dpi); + } + + public Image(final BufferedImage image, final float dpiX, final 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, final float x, final float y) throws IOException + { + if (imageXObject == null) { + if(quality == 1f) { + imageXObject = LosslessFactory.createFromImage(doc, image); + } else { + imageXObject = JPEGFactory.createFromImage(doc, image, quality); + } + } + 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(final float width) { + float factorWidth = width / this.width; + return scale(width, this.height * factorWidth); + } + + /** + * 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(final float height) { + float factorHeight = height / this.height; + return scale(this.width * factorHeight, height); + } + + public float getImageWidthInPoints(final float dpiX) { + return this.width * 72f / dpiX; + } + + public float getImageHeightInPoints(final 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(final float boundWidth, final float boundHeight) { + float[] imageDimension = 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; + } + + public void setQuality(final float quality) throws IllegalArgumentException { + if(quality <= 0 || quality > 1) { + throw new IllegalArgumentException( + "The quality value must be configured greater than zero and less than or equal to 1"); + } + this.quality = quality; + } + + // -- HELPER + + private void scaleImageFromPixelToPoints() { + float dpiX = dpi[0]; + float dpiY = dpi[1]; + scale(getImageWidthInPoints(dpiX), getImageHeightInPoints(dpiY)); + } + + /** + * Scales {@link Image} to desired dimensions + * @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. + */ + private static float[] getScaledDimension(final float imageWidth, final float imageHeight, final float boundWidth, final 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/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/ImageCell.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/ImageCell.java new file mode 100644 index 00000000000..af4c832c840 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/ImageCell.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import org.apache.causeway.extensions.tabular.pdf.factory.HorizontalAlignment; +import org.apache.causeway.extensions.tabular.pdf.factory.VerticalAlignment; + +import lombok.Getter; + +class ImageCell extends Cell { + + @Getter private Image image; + + ImageCell(final Row row, final float width, final Image image, final boolean isCalculated) { + super(row, width, null, isCalculated); + this.image = image; + if(image.getWidth() > getInnerWidth()){ + scaleToFit(); + } + } + + public void scaleToFit() { + this.image = image.scaleByWidth(getInnerWidth()); + } + + ImageCell(final Row row, final float width, final Image image, final boolean isCalculated, + final HorizontalAlignment align, final VerticalAlignment valign) { + super(row, width, null, isCalculated, align, valign); + this.image = image; + if(image.getWidth() > getInnerWidth()) { + scaleToFit(); + } + } + + @Override + public float getTextHeight() { + return image.getHeight(); + } + + @Override + public float getHorizontalFreeSpace() { + return getInnerWidth() - image.getWidth(); + } + + @Override + public float getVerticalFreeSpace() { + return getInnerHeight() - image.getHeight(); + } + +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PDStreamUtils.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PDStreamUtils.java new file mode 100644 index 00000000000..c8f7916c316 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PDStreamUtils.java @@ -0,0 +1,144 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import java.awt.Color; +import java.io.IOException; + +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDFont; + +import org.apache.causeway.extensions.tabular.pdf.factory.FontUtils; +import org.apache.causeway.extensions.tabular.pdf.factory.LineStyle; + +import lombok.experimental.UtilityClass; + +/** + * Utility methods for {@link PDPageContentStream} + */ +@UtilityClass +class 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/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PageContentStreamOptimized.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PageContentStreamOptimized.java new file mode 100644 index 00000000000..c3c0ab256f6 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PageContentStreamOptimized.java @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +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 lombok.extern.log4j.Log4j2; + +import java.awt.Color; +import java.io.IOException; +import java.util.Arrays; + +@Log4j2 +final 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(final PDPageContentStream pageContentStream) { + this.pageContentStream = pageContentStream; + } + + public void setRotated(final 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(final PDFont font, final float fontSize) throws IOException { + if (font != currentFont || fontSize != currentFontSize) { + pageContentStream.setFont(font, fontSize); + currentFont = font; + currentFontSize = fontSize; + } + } + + public void showText(final String text) throws IOException { + beginText(); + try { + pageContentStream.showText(text); + } catch (IllegalArgumentException e) { + log.warn("cannot render text text '{}' -> {}", text, e.getMessage()); + } + } + + public void newLineAt(final float tx, final 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(final PDImageXObject image, final float x, final float y, final float width, final float height) throws IOException { + endText(); + pageContentStream.drawImage(image, x, y, width, height); + } + + private Color currentStrokingColor; + + public void setStrokingColor(final Color color) throws IOException { + if (color != currentStrokingColor) { + pageContentStream.setStrokingColor(color); + currentStrokingColor = color; + } + } + + private Color currentNonStrokingColor; + + public void setNonStrokingColor(final Color color) throws IOException { + if (color != currentNonStrokingColor) { + pageContentStream.setNonStrokingColor(color); + currentNonStrokingColor = color; + } + } + + public void addRect(final float x, final float y, final float width, final float height) throws IOException { + endText(); + pageContentStream.addRect(x, y, width, height); + } + + public void moveTo(final float x, final float y) throws IOException { + endText(); + pageContentStream.moveTo(x, y); + } + + public void lineTo(final float x, final 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(final float lineWidth) throws IOException { + if (lineWidth != currentLineWidth) { + endText(); + pageContentStream.setLineWidth(lineWidth); + currentLineWidth = lineWidth; + } + } + + private int currentLineCapStyle = -1; + + public void setLineCapStyle(final int lineCapStyle) throws IOException { + if (lineCapStyle != currentLineCapStyle) { + endText(); + pageContentStream.setLineCapStyle(lineCapStyle); + currentLineCapStyle = lineCapStyle; + } + } + + private float[] currentLineDashPattern; + private float currentLineDashPhase; + + public void setLineDashPattern(final float[] pattern, final 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/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PageProvider.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PageProvider.java new file mode 100644 index 00000000000..35cac6d8375 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PageProvider.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.common.PDRectangle; + +import lombok.Getter; + +final class PageProvider { + + @Getter private final PDDocument document; + private final PDRectangle size; + private int currentPageIndex = -1; + + PageProvider(final PDDocument document, final PDRectangle size) { + this.document = document; + this.size = size; + } + + public PDPage createPage() { + currentPageIndex = document.getNumberOfPages(); + return getCurrentPage(); + } + + public PDPage nextPage() { + if (currentPageIndex == -1) { + currentPageIndex = document.getNumberOfPages(); + } else { + currentPageIndex++; + } + return getCurrentPage(); + } + + 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/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Paragraph.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Paragraph.java new file mode 100644 index 00000000000..e95b7342941 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Paragraph.java @@ -0,0 +1,715 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +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; + +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.font.Standard14Fonts; + +import org.apache.causeway.extensions.tabular.pdf.factory.FontUtils; +import org.apache.causeway.extensions.tabular.pdf.factory.HorizontalAlignment; +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Token.TokenType; + +final class Paragraph { + + enum TextType { + HIGHLIGHT,UNDERLINE,SQUIGGLY,STRIKEOUT; + } + + /** + * Data container for HTML ordered list elements. + */ + record HTMLListNode( + /** Element's current ordering number (e.g third element in the current list) */ + int orderingNumber, + /** Element's whole ordering number value (e.g 1.1.2.1) */ + String value) { + } + + 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 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 Map> mapLineTokens = new LinkedHashMap<>(); + private float maxLineWidth = Integer.MIN_VALUE; + private List tokens; + private List lines; + private Float spaceWidth; + + public Paragraph(final String text, final PDFont font, final float fontSize, final 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 = t -> t.split("(?<=\\s|-|@|,|\\.|:|;)"); + + public Paragraph(final String text, final PDFont font, final int fontSize, final int width) { + this(text, font, fontSize, width, HorizontalAlignment.LEFT, null); + } + + public Paragraph(final String text, final PDFont font, final float fontSize, final float width, final HorizontalAlignment align, + final WrappingFunction wrappingFunction) { + this(text, font, fontSize, width, align, Color.BLACK, (TextType) null, wrappingFunction); + } + + public Paragraph(final String text, final PDFont font, final float fontSize, final float width, final HorizontalAlignment align, + final Color color, final TextType textType, final WrappingFunction wrappingFunction) { + this(text, font, fontSize, width, align, color, textType, wrappingFunction, 1); + } + + public Paragraph(final String text, final PDFont font, final float fontSize, final float width, final HorizontalAlignment align, + final Color color, final TextType textType, final WrappingFunction wrappingFunction, final 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 = new PDType1Font(Standard14Fonts.FontName.HELVETICA_BOLD); + fontItalic = new PDType1Font(Standard14Fonts.FontName.HELVETICA_OBLIQUE); + fontBoldItalic = new PDType1Font(Standard14Fonts.FontName.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.type()) { + 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.text().equals("ol")) { + numberOfOrderedLists++; + if(listLevel > 1){ + stack.add(new HTMLListNode(orderListElement-1, stack.isEmpty() ? String.valueOf(orderListElement-1)+"." : stack.peek().value() + String.valueOf(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.text().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.text().equals("ol")) { + numberOfOrderedLists--; + // reset elements + if(numberOfOrderedLists>0){ + orderListElement = stack.peek().orderingNumber()+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() ? String.valueOf(orderListElement) + "." : stack.pop().value() + "."; + 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() ? String.valueOf(orderListElement) + "." : stack.peek().value() + "." + String.valueOf(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() ? String.valueOf("1") + "." : stack.pop().value() + ". "; + 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().value() + String.valueOf(orderListElement) + ". "; + } else { + orderingNumber = String.valueOf(orderListElement) + ". "; + } + textInLine.push(currentFont, fontSize, new Token(TokenType.ORDERING, orderingNumber)); + orderListElement++; + } else { + // if it's unordered list then just move by bullet character (take care of alignment!) + textInLine.push(currentFont, fontSize, new Token(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().value() + String.valueOf(orderListElement) + ". "; + } else { + orderingNumber = String.valueOf(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.text(); + 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(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(word)); + } else { + sinceLastWrapPoint.push(currentFont, fontSize, token); + } + + } catch (IOException e) { + e.printStackTrace(); + } + break; + case BULLET, ORDERING, PADDING: + 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.text()); + } + + private static boolean isBold(final Token token) { + return "b".equals(token.text()); + } + + private static boolean isParagraph(final Token token) { + return "p".equals(token.text()); + } + + private static boolean isListElement(final Token token) { + return "li".equals(token.text()); + } + + private static boolean isList(final Token token) { + return "ul".equals(token.text()) || "ol".equals(token.text()); + } + + private float indentLevel(final int numberOfSpaces) throws IOException { + if (spaceWidth == null) { + spaceWidth = font.getSpaceWidth(); + } + return numberOfSpaces * spaceWidth; + } + + public PDFont getFont(final boolean isBold, final 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, final 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); + } + + // 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; + } + + 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(final HorizontalAlignment align) { + invalidateLineWrapping(); + this.align = align; + } + + public boolean isDrawDebug() { + return drawDebug; + } + + public void setDrawDebug(final boolean drawDebug) { + this.drawDebug = drawDebug; + } + + public WrappingFunction getWrappingFunction() { + return wrappingFunction == null ? DEFAULT_WRAP_FUNC : wrappingFunction; + } + + public float getMaxLineWidth() { + return maxLineWidth; + } + + public float getLineWidth(final int key) { + return lineWidths.get(key); + } + + public Map> getMapLineTokens() { + return mapLineTokens; + } + + public float getLineSpacing() { + return lineSpacing; + } + + public void setLineSpacing(final float lineSpacing) { + this.lineSpacing = lineSpacing; + } + +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PipelineLayer.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PipelineLayer.java new file mode 100644 index 00000000000..2acecbace4a --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/PipelineLayer.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.pdmodel.font.PDFont; + +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Token.TokenType; + +final class PipelineLayer { + + private final StringBuilder text = new StringBuilder(); + private String lastTextToken = ""; + private 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.type().equals(TokenType.PADDING)) { + width += Float.parseFloat(token.text()); + } + if (token.type().equals(TokenType.BULLET)) { + // just appending one space because our bullet width will be wide as one character of current font + text.append(token.text()); + width += (token.getWidth(font) / 1000f * fontSize); + } + + if (token.type().equals(TokenType.ORDERING)) { + // just appending one space because our bullet width will be wide as one character of current font + text.append(token.text()); + width += (token.getWidth(font) / 1000f * fontSize); + } + + if (token.type().equals(TokenType.TEXT)) { + text.append(lastTextToken); + width += widthLastToken; + lastTextToken = token.text(); + 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() + "]"; + } + + // -- HELPER + + private static String rtrim(final 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); + } +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Row.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Row.java new file mode 100644 index 00000000000..d4431ed31de --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Row.java @@ -0,0 +1,303 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; + +import org.apache.causeway.extensions.tabular.pdf.factory.HorizontalAlignment; +import org.apache.causeway.extensions.tabular.pdf.factory.VerticalAlignment; + +public class Row { + + private final Table table; + PDOutlineItem bookmark; + List cells; + private boolean headerRow = false; + float height; + private float lineSpacing = 1; + + Row(final Table table, final List cells, final float height) { + this.table = table; + this.cells = cells; + this.height = height; + } + + Row(final Table table, final 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(final float width, final String value) { + var 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(final float width, final BufferedImage img) { + var cell = new ImageCell(this, width, new Image(img), true); + setBorders(cell, cells.isEmpty()); + cells.add(cell); + return cell; + } + + public ImageCell createImageCell(final float width, final Image img, final HorizontalAlignment align, final VerticalAlignment valign) { + var 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(final float width, final String tableData, final PDDocument doc, final PDPage page, final float yStart, + final float pageTopMargin, final float pageBottomMargin) { + var 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(final float width, final String value, final HorizontalAlignment align, final VerticalAlignment valign) { + var 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(final String value) { + float headerCellWidth = table.getHeader().getCells().get(cells.size()).getWidth(); + var 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 (var cell : cells) { + cell.setTopBorderStyle(null); + } + } + + /** + *

    + * Remove all borders of cells. + *

    + */ + void removeAllBorders() { + for (var 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 (var 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(final float height) { + this.height = height; + } + + public List getCells() { + return cells; + } + + public int getColCount() { + return cells.size(); + } + + public void setCells(final List cells) { + this.cells = cells; + } + + public float getWidth() { + return table.getWidth(); + } + + public PDOutlineItem getBookmark() { + return bookmark; + } + + public void setBookmark(final PDOutlineItem bookmark) { + this.bookmark = bookmark; + } + + protected float getLastCellExtraWidth() { + float cellWidth = 0; + for (var 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(final boolean headerRow) { + this.headerRow = headerRow; + } + + public float getLineSpacing() { + return lineSpacing; + } + + public void setLineSpacing(final float lineSpacing) { + this.lineSpacing = lineSpacing; + } +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Table.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Table.java new file mode 100644 index 00000000000..fb9961e48dd --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Table.java @@ -0,0 +1,911 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import java.awt.Color; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +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.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +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.PDPageXYZDestination; +import org.apache.pdfbox.pdmodel.interactive.documentnavigation.outline.PDOutlineItem; + +import org.apache.causeway.extensions.tabular.pdf.factory.FontUtils; +import org.apache.causeway.extensions.tabular.pdf.factory.HorizontalAlignment; +import org.apache.causeway.extensions.tabular.pdf.factory.LineStyle; + +public final class Table { + + record Options( + float yStartNewPage, + float pageTopMargin, + float pageBottomMargin, + float width, + float margin, + PageProvider pageProvider, + boolean isDummy) { + } + + public static Table create(final float yStart, final float yStartNewPage, final float pageTopMargin, final float bottomMargin, + final float width, final float margin, final PDDocument document, final PDPage currentPage) throws IOException { + var opts = new Table.Options(yStartNewPage, pageTopMargin, bottomMargin, width, margin, new PageProvider(document, currentPage.getMediaBox()), false); + return new Table(yStart, opts, currentPage); + } + + public static Table dummy(final float yStart, final float yStartNewPage, final float pageTopMargin, final float bottomMargin, + final float width, final float margin, final PDDocument document, final PDPage currentPage) throws IOException { + var opts = new Table.Options(yStartNewPage, pageTopMargin, bottomMargin, width, margin, new PageProvider(document, currentPage.getMediaBox()), true); + return new Table(yStart, opts, currentPage); + } + + + private final PDDocument document; + private float margin; + + private PDPage currentPage; + private PageContentStreamOptimized tableContentStream; + private List bookmarks; + private List header = new ArrayList<>(); + private List rows = new ArrayList<>(); + + private final float yStartNewPage; + private float yStart; + private final float width; + private final boolean drawLines; + private final boolean drawContent; + private float headerBottomMargin = 4f; + private float lineSpacing = 1f; + + private boolean tableIsBroken = false; + private boolean tableStartedAtNewPage = false; + private boolean removeTopBorders = false; + private boolean removeAllBorders = false; + + private PageProvider pageProvider; + + // page margins + private final float pageTopMargin; + private final float pageBottomMargin; + + private boolean drawDebug; + + private Table( + final float yStart, + final Options opts, + final PDPage currentPage) throws IOException { + this.pageTopMargin = opts.pageTopMargin(); + this.pageBottomMargin = opts.pageBottomMargin(); + this.document = opts.pageProvider().getDocument(); + this.drawLines = !opts.isDummy(); + this.drawContent = !opts.isDummy(); + // Initialize table + this.yStartNewPage = opts.yStartNewPage(); + this.margin = opts.margin(); + this.width = opts.width(); + this.pageProvider = opts.pageProvider(); + + this.currentPage = currentPage; + this.yStart = yStart; + } + + protected PDType0Font loadFont(final String fontPath) throws IOException { + return FontUtils.loadFont(getDocument(), fontPath); + } + + protected PDDocument getDocument() { + return document; + } + + public void drawTitle(final String title, final PDFont font, final int fontSize, final float tableWidth, final float height, + final String alignment, + final float freeSpaceForPageBreak, final boolean drawHeaderMargin) throws IOException { + drawTitle(title, font, fontSize, tableWidth, height, alignment, freeSpaceForPageBreak, null, drawHeaderMargin); + } + + public void drawTitle(final String title, final PDFont font, final int fontSize, final float tableWidth, final float height, + final String alignment, + final float freeSpaceForPageBreak, final WrappingFunction wrappingFunction, final 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(final float height) { + var row = new Row(this, height); + row.setLineSpacing(lineSpacing); + this.rows.add(row); + return row; + } + + public Row createRow(final List cells, final float height) { + var 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 (var 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(final 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 (var 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. + */ + private PDPage createNewPage() { + return pageProvider.nextPage(); + } + + private PageContentStreamOptimized createPdPageContentStream() throws IOException { + return new PageContentStreamOptimized( + new PDPageContentStream(getDocument(), getCurrentPage(), + PDPageContentStream.AppendMode.APPEND, true)); + } + + private void drawCellContent(final Row row, final float rowHeight) throws IOException { + + // position into first cell (horizontal) + float cursorX = margin; + float cursorY; + + for (var cell : row.getCells()) { + // remember horizontal cursor position, so we can advance to the + // next cell easily later + float cellStartX = cursorX; + if (cell instanceof ImageCell imageCell) { + + cursorY = yStart - cell.getTopPadding() + - (cell.getTopBorderStyle() == null ? 0 : cell.getTopBorderStyle().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.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().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); + + if (imageCell.getUrl() != null) { + List annotations = currentPage.getAnnotations(); + + PDBorderStyleDictionary borderULine = new PDBorderStyleDictionary(); + borderULine.setStyle(PDBorderStyleDictionary.STYLE_UNDERLINE); + borderULine.setWidth(1); // 1 point + + PDAnnotationLink txtLink = new PDAnnotationLink(); + txtLink.setBorderStyle(borderULine); + + // 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, (imageCell.getImage().getWidth()), -(imageCell.getImage().getHeight())); + txtLink.setRectangle(position); + + // add an action + PDActionURI action = new PDActionURI(); + action.setURI(imageCell.getUrl().toString()); + txtLink.setAction(action); + annotations.add(txtLink); + } + + } else if (cell instanceof TableCell tableCell) { + + cursorY = yStart - cell.getTopPadding() + - (cell.getTopBorderStyle() != null ? cell.getTopBorderStyle().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.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().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.getTopBorderStyle() != null ? cell.getTopBorderStyle().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.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().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.getTopBorderStyle() == null ? 0 : cell.getTopBorderStyle().getWidth()); + + if (drawDebug) { + // @formatter:off + // top padding + PDStreamUtils.rect(tableContentStream, cursorX + (cell.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().getWidth()), yStart - (cell.getTopBorderStyle() == null ? 0 : cell.getTopBorderStyle().getWidth()), cell.getWidth() - (cell.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().getWidth()) - (cell.getRightBorderStyle() == null ? 0 : cell.getRightBorderStyle().getWidth()), cell.getTopPadding(), Color.RED); + // bottom padding + PDStreamUtils.rect(tableContentStream, cursorX + (cell.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().getWidth()), yStart - cell.getHeight() + (cell.getBottomBorderStyle() == null ? 0 : cell.getBottomBorderStyle().getWidth()) + cell.getBottomPadding(), cell.getWidth() - (cell.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().getWidth()) - (cell.getRightBorderStyle() == null ? 0 : cell.getRightBorderStyle().getWidth()), cell.getBottomPadding(), Color.RED); + // left padding + PDStreamUtils.rect(tableContentStream, cursorX + (cell.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().getWidth()), yStart - (cell.getTopBorderStyle() == null ? 0 : cell.getTopBorderStyle().getWidth()), cell.getLeftPadding(), cell.getHeight() - (cell.getTopBorderStyle() == null ? 0 : cell.getTopBorderStyle().getWidth()) - (cell.getBottomBorderStyle() == null ? 0 : cell.getBottomBorderStyle().getWidth()), Color.RED); + // right padding + PDStreamUtils.rect(tableContentStream, cursorX + cell.getWidth() - (cell.getRightBorderStyle() == null ? 0 : cell.getRightBorderStyle().getWidth()), yStart - (cell.getTopBorderStyle() == null ? 0 : cell.getTopBorderStyle().getWidth()), -cell.getRightPadding(), cell.getHeight() - (cell.getTopBorderStyle() == null ? 0 : cell.getTopBorderStyle().getWidth()) - (cell.getBottomBorderStyle() == null ? 0 : cell.getBottomBorderStyle().getWidth()), Color.RED); + // @formatter:on + } + + // respect left padding + cursorX += cell.getLeftPadding() + + (cell.getLeftBorderStyle() == null ? 0 : cell.getLeftBorderStyle().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; + } + + if (cell.getUrl() != null) { + List annotations = 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, (cell.getWidth()), -(cell.getHeight())); + txtLink.setRectangle(position); + + // add an action + PDActionURI action = new PDActionURI(); + action.setURI(cell.getUrl().toString()); + txtLink.setAction(action); + annotations.add(txtLink); + } + + } + + // 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.type()) { + case OPEN_TAG: + if ("b".equals(token.text())) { + boldCounter++; + } else if ("i".equals(token.text())) { + italicCounter++; + } + break; + case CLOSE_TAG: + if ("b".equals(token.text())) { + boldCounter = Math.max(boldCounter - 1, 0); + } else if ("i".equals(token.text())) { + italicCounter = Math.max(italicCounter - 1, 0); + } + break; + case PADDING: + cursorX += Float.parseFloat(token.text()); + 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.text()); + cursorY += token.getWidth(currentFont) / 1000 * cell.getFontSize(); + } else { + this.tableContentStream.newLineAt(cursorX, cursorY); + this.tableContentStream.showText(token.text()); + 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.text()); + cursorY += token.getWidth(currentFont) / 1000 * cell.getFontSize(); + } else { + try { + this.tableContentStream.newLineAt(cursorX, cursorY); + this.tableContentStream.showText(token.text()); + cursorX += token.getWidth(currentFont) / 1000 * cell.getFontSize(); + } catch (IOException e) { + e.printStackTrace(); + } + } + break; + case POSSIBLE_WRAP_POINT, WRAP_POINT: + break; + } + } + if (cell.isTextRotated()) { + cursorX = cursorX + cell.getParagraph().getFontHeight() * cell.getLineSpacing(); + } else { + cursorY = cursorY - cell.getParagraph().getFontHeight() * cell.getLineSpacing(); + } + } + } + + PDRectangle rectangle = new PDRectangle(cellStartX, yStart - cell.getHeight(), cell.getWidth(), cell.getHeight()); + cell.notifyContentDrawnListeners(getDocument(), getCurrentPage(), rectangle); + + // 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(final Row row, final float rowHeight) throws IOException { + float xStart = margin; + + Iterator cellIterator = row.getCells().iterator(); + while (cellIterator.hasNext()) { + var 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(final float rowHeight, final Cell cell, final float xStart) throws IOException { + + float yEnd = yStart - rowHeight; + + // top + LineStyle topBorder = cell.getTopBorderStyle(); + if (topBorder != null) { + float y = yStart - topBorder.getWidth() / 2; + drawLine(xStart, y, xStart + cell.getWidth(), y, topBorder); + } + + // right + LineStyle rightBorder = cell.getRightBorderStyle(); + 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.getBottomBorderStyle(); + 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.getLeftBorderStyle(); + if (leftBorder != null) { + float x = xStart + leftBorder.getWidth() / 2; + drawLine(x, yStart, x, yEnd + (bottomBorder == null ? 0 : bottomBorder.getWidth()), leftBorder); + } + + } + + private void drawLine(final float xStart, final float yStart, final float xEnd, final float yEnd, final LineStyle border) throws IOException { + PDStreamUtils.setLineStyles(tableContentStream, border); + tableContentStream.moveTo(xStart, yStart); + tableContentStream.lineTo(xEnd, yEnd); + tableContentStream.stroke(); + } + + private void fillCellColor(final Cell cell, float yStart, final float xStart, final float rowHeight, final 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.getTopBorderStyle() == null ? 0 : cell.getTopBorderStyle().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 PDPage getCurrentPage() { + if (this.currentPage == null) { + throw new NullPointerException("No current page defined."); + } + return this.currentPage; + } + + private boolean isEndOfPage(final 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(final PDOutlineItem bookmark) { + if (bookmarks == null) { + bookmarks = new ArrayList<>(); + } + bookmarks.add(bookmark); + } + + public List getBookmarks() { + return bookmarks; + } + + /** + * 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 (var 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 (var 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(final 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(final float yStart) { + this.yStart = yStart; + } + + public boolean isDrawDebug() { + return drawDebug; + } + + public void setDrawDebug(final boolean drawDebug) { + this.drawDebug = drawDebug; + } + + public boolean tableIsBroken() { + return tableIsBroken; + } + + public void setTableIsBroken(final boolean tableIsBroken) { + this.tableIsBroken = tableIsBroken; + } + + public List getRows() { + return rows; + } + + public boolean tableStartedAtNewPage() { + return tableStartedAtNewPage; + } + + public float getLineSpacing() { + return lineSpacing; + } + + public void setLineSpacing(final float lineSpacing) { + this.lineSpacing = lineSpacing; + } + + public boolean allBordersRemoved() { + return removeAllBorders; + } + + public void removeAllBorders(final boolean removeAllBorders) { + this.removeAllBorders = removeAllBorders; + } + +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/TableCell.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/TableCell.java new file mode 100644 index 00000000000..76e916ebd65 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/TableCell.java @@ -0,0 +1,443 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +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.font.PDFont; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import org.apache.causeway.extensions.tabular.pdf.factory.FontUtils; +import org.apache.causeway.extensions.tabular.pdf.factory.HorizontalAlignment; +import org.apache.causeway.extensions.tabular.pdf.factory.VerticalAlignment; + +import lombok.extern.log4j.Log4j2; + +@Log4j2 +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 float marginBetweenElementsY = FontUtils.getHeight(getFont(), getFontSize()); + private final HorizontalAlignment align; + private PageContentStreamOptimized tableCellContentStream; + + // page margins + private final float pageTopMargin; + private final float pageBottomMargin; + // default title fonts + private int tableTitleFontSize = 8; + + TableCell(final Row row, final float width, final String tableData, final boolean isCalculated, final PDDocument document, + final PDPage page, + final float yStart, final float pageTopMargin, final float pageBottomMargin) { + this(row, width, tableData, isCalculated, document, page, yStart, pageTopMargin, pageBottomMargin, + HorizontalAlignment.LEFT, VerticalAlignment.TOP); + } + + TableCell(final Row row, final float width, final String tableData, final boolean isCalculated, final PDDocument document, + final PDPage page, + final float yStart, final float pageTopMargin, final float pageBottomMargin, final HorizontalAlignment align, + final VerticalAlignment valign) { + 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; + 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() { + try { + // 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) { + if (chunkie.contains(") + row.setHeaderRow(true); + } + int columnsSize = tableHasHeaderColumns ? tableHeaderCols.size() : tableCols.size(); + // calculate how much really columns do you have (including + // colspans!) + for (Element col : tableHasHeaderColumns ? tableHeaderCols : tableCols) { + if (col.attr("colspan") != null && !col.attr("colspan").isEmpty()) { + columnsSize += Integer.parseInt(col.attr("colspan")) - 1; + } + } + for (Element col : tableHasHeaderColumns ? tableHeaderCols : tableCols) { + if (col.attr("colspan") != null && !col.attr("colspan").isEmpty()) { + row.createCell( + tableWidth / columnsSize * Integer.parseInt(col.attr("colspan")) / row.getWidth() * 100, + col.html().replace("&", "&")); + } else { + row.createCell(tableWidth / columnsSize / row.getWidth() * 100, + col.html().replace("&", "&")); + } + } + yStart -= row.getHeight(); + } + if (drawTable) { + table.draw(); + } + + height += table.getHeaderAndDataHeight() + marginBetweenElementsY; + } + + /** + * 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(final Paragraph paragraph, final 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()) - (getTopBorderStyle() == null ? 0 : getTopBorderStyle().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.type()) { + case OPEN_TAG: + if ("b".equals(token.text())) { + boldCounter++; + } else if ("i".equals(token.text())) { + italicCounter++; + } + break; + case CLOSE_TAG: + if ("b".equals(token.text())) { + boldCounter = Math.max(boldCounter - 1, 0); + } else if ("i".equals(token.text())) { + italicCounter = Math.max(italicCounter - 1, 0); + } + break; + case PADDING: + cursorX += Float.parseFloat(token.text()); + 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.text()); + } + cursorY += token.getWidth(currentFont) / 1000 * getFontSize(); + } else { + // if it is not calculation then draw it + if (!onlyCalculateHeight) { + tableCellContentStream.newLineAt(cursorX, cursorY); + tableCellContentStream.showText(token.text()); + } + 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.text()); + } + cursorY += token.getWidth(currentFont) / 1000 * getFontSize(); + } else { + if (!onlyCalculateHeight) { + tableCellContentStream.newLineAt(cursorX, cursorY); + tableCellContentStream.showText(token.text()); + } + cursorX += token.getWidth(currentFont) / 1000 * getFontSize(); + } + break; + case POSSIBLE_WRAP_POINT, WRAP_POINT: + 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(final PDPage page) { + try { + // 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) { + if (chunkie.contains(" cache) { + + public enum TokenType { + TEXT, POSSIBLE_WRAP_POINT, WRAP_POINT, OPEN_TAG, CLOSE_TAG, PADDING, BULLET, ORDERING + } + + public static Token text(final String text) { + return new Token(TokenType.TEXT, text, new ConcurrentHashMap<>()); + } + + public Token(final TokenType type, final String text) { + this(type, text, new ConcurrentHashMap<>()); + } + + public float getWidth(final PDFont font) throws IOException { + return cache.computeIfAbsent(font, this::computeWidth); + } + + private float computeWidth(final PDFont font) { + try { + return font.getStringWidth(text); + } catch (Exception e) { + // if text contains characters that are unavailable with given font, fallback to an arbitrary placeholder character + try { + return font.getStringWidth("X") * text.length(); + } catch (IOException e1) { + return 1.f; + } + } + } + +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Tokenizer.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Tokenizer.java new file mode 100644 index 00000000000..ada68963720 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/Tokenizer.java @@ -0,0 +1,330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Stack; + +import org.apache.causeway.extensions.tabular.pdf.factory.internal.Token.TokenType; + +import lombok.experimental.UtilityClass; + +@UtilityClass +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"); + + public static List tokenize(final String text, final WrappingFunction wrappingFunction) { + if(text == null) return Collections.emptyList(); + + final List tokens = new ArrayList<>(); + 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(sb.toString())); + sb.delete(0, sb.length()); + } + tokens.add(POSSIBLE_WRAP_POINT); + currentWrapPoint = possibleWrapPoints.pop(); + } + final char c = text.charAt(textIndex); + switch (c) { + case '<': + 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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(sb.toString())); + sb.delete(0, sb.length()); + } + tokens.add(CLOSE_TAG_LI); + textIndex += 4; + consumed = true; + } + } + } + } + + } + if (!consumed) { + sb.append('<'); + } + break; + default: + sb.append(c); + break; + } + textIndex++; + } + + if (sb.length() > 0) { + tokens.add(Token.text(sb.toString())); + sb.delete(0, sb.length()); + } + tokens.add(POSSIBLE_WRAP_POINT); + + return tokens; + } + + // -- HELPER + + private static boolean isWrapPointChar(final char ch) { + return + ch == ' ' || + ch == ',' || + ch == '.' || + ch == '-' || + ch == '@' || + ch == ':' || + ch == ';' || + ch == '\n' || + ch == '\t' || + ch == '\r' || + ch == '\f' || + ch == '\u000B'; + } + + private static Stack findWrapPoints(final 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(final String text, final 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; + } + +} diff --git a/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/WrappingFunction.java b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/WrappingFunction.java new file mode 100644 index 00000000000..a379d9e7691 --- /dev/null +++ b/extensions/vw/tabular/pdf/src/main/java/org/apache/causeway/extensions/tabular/pdf/factory/internal/WrappingFunction.java @@ -0,0 +1,24 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.extensions.tabular.pdf.factory.internal; + +interface WrappingFunction { + + String[] getLines(String text); +}