package org.simantics.document.linking.report.pdf; import java.awt.Color; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.font.LineBreakMeasurer; import java.awt.font.TextAttribute; import java.awt.font.TextLayout; import java.net.URL; import java.text.AttributedCharacterIterator; import java.text.AttributedString; import java.util.ArrayList; import java.util.Collections; import java.util.Hashtable; import java.util.List; import org.simantics.document.linking.report.Document.TextSize; import org.simantics.document.linking.report.Table; import org.simantics.document.linking.report.TableColumn; import org.simantics.document.linking.report.TableColumn.Alignment; import org.simantics.document.linking.report.TableRow; import org.simantics.document.linking.report.TextItem; import org.simantics.document.linking.report.URLItem; import com.lowagie.text.pdf.PdfAction; public class PDFTable implements Table, PDFElement { PDFDocument writer; PDFPageStream stream; PDFPage startPage; int currentLine = 0; List columns = new ArrayList(); List columnNames = new ArrayList(); List columnSizes = new ArrayList(); List columnPositions = new ArrayList(); TextItem title = null; boolean headerVisible = true; private boolean linesVisible = true; private boolean linesPrevVisible = true; boolean clipText = false; int textOffsetX = 2; int textOffsetY = 2; public PDFTable(PDFDocument writer, PDFPageStream stream) { this.writer = writer; this.stream = stream; this.startPage = stream.getCurrentPage(); } public PDFTable(PDFTable table) { this.writer = table.writer; this.stream = table.stream; this.startPage = stream.getCurrentPage(); this.columns.addAll(table.columns); this.columnNames.addAll(table.columnNames); updateColumnPositions(); } @Override public PDFPage getPage() { return startPage; } /* (non-Javadoc) * @see org.simantics.document.linking.report.Table#addColumn(java.lang.String, double) */ @Override public TableColumn addColumn(String name, double width) { TableColumn tc = new TableColumn(name, width); columns.add(tc); columnNames.add(name); updateColumnPositions(); return tc; } private void updateColumnPositions() { int pos = 0; columnSizes.clear(); columnPositions.clear(); for (TableColumn c : columns) { int size = (int)(c.getWidth()*stream.getContentWidth()); columnSizes.add(size); columnPositions.add(pos); pos+=size; } } @Override public List getColumns() { return columns; } @Override public boolean isLinesVisible() { return linesVisible; } @Override public void setLinesVisible(boolean b) { if (this.linesVisible == b) return; this.linesPrevVisible = linesVisible; this.linesVisible = b; } @Override public boolean isHeaderVisible() { return headerVisible; } @Override public void setHeaderVisible(boolean b) { this.headerVisible = b; } private boolean isFirstLine() { return currentLine == 0 || stream.getCurrentPage().currentLine == 1; } @Override public void setTitle(String title) { try { this.title = writer.newItem(TextItem.class); this.title.setText(title); } catch (Exception e) { } } @Override public void setTitle(TextItem title){ this.title = title; } /* (non-Javadoc) * @see org.simantics.document.linking.report.Table#writeRow(java.lang.String[]) */ @Override public TableRow writeRow(String... line) throws Exception{ List list = new ArrayList(line.length); for (String s : line) list.add(s); return writeRow(list); } /* (non-Javadoc) * @see org.simantics.document.linking.report.Table#writeRow(java.util.List) */ @Override public TableRow writeRow(List line) throws Exception{ if (isFirstLine()) writeHeader(); return _writeRow(line); } @Override public TableRow writeRowItem(TextItem... line) throws Exception { List list = new ArrayList(line.length); for (TextItem s : line) list.add(s); return writeRowItem(list); } @Override public TableRow writeRowItem(List line) throws Exception { if (isFirstLine()) writeHeader(); return _writeRow2(line); } private TableRow _writeRow(List line) throws Exception { int h = getTextHeight(); int ht = getTopHeight(); int hb = getBottomHeight(); PDFPage page = getCurrentPage(); Graphics2D g2d = page.g2d; Shape clip = g2d.getClip(); if (clipText) { for (int i = 0; i < line.size(); i++) { if (line.get(i) == null) continue; g2d.setClip(columnPositions.get(i),ht-1,columnSizes.get(i),hb-ht+2); g2d.drawString(line.get(i), columnPositions.get(i)+textOffsetX, h); } g2d.setClip(clip); if (linesVisible) { for (int i = 0; i < line.size(); i++) { g2d.drawLine(columnPositions.get(i), ht, columnPositions.get(i), hb); } if (isFirstLine() || !linesPrevVisible) { g2d.drawLine(0, ht, stream.contentWidth, ht); linesPrevVisible = true; } g2d.drawLine(stream.contentWidth, ht, stream.contentWidth, hb); g2d.drawLine(0, hb, stream.contentWidth, hb); } currentLine++; page.currentLine++; page.availableLines--; page.currentPixel += getLineHeight(); } else { PositionedRow row = _getRow(line); if (stream.contentHeight-page.currentPixel < row.reservedSpace) { stream.nextPage(); page = getCurrentPage(); g2d = page.g2d; writeHeader(); row = _getRow(line); } row.render(g2d); currentLine+= row.realLines; page.currentLine+= row.realLines; page.currentPixel += row.reservedSpace; page.estimateAvailableLines(); } stream.checkNextPage(); return new PDFTableRow(); } private TableRow _writeRow2(List line) throws Exception { int h = getTextHeight(); int ht = getTopHeight(); int hb = getBottomHeight(); PDFPage page = getCurrentPage(); Graphics2D g2d = page.g2d; Shape clip = g2d.getClip(); if (clipText) { for (int i = 0; i < line.size(); i++) { TextItem text = line.get(i); if (text == null) continue; g2d.setClip(columnPositions.get(i),ht,columnSizes.get(i),hb-ht); g2d.drawString(text.getText(), columnPositions.get(i)+textOffsetX, h); if (text instanceof URLItem) { URL url = ((URLItem)text).getURL(); if (url != null) { addLink(url, columnPositions.get(i),ht,columnSizes.get(i),hb-ht); } } } g2d.setClip(clip); if (linesVisible) { for (int i = 0; i < line.size(); i++) { g2d.drawLine(columnPositions.get(i), ht, columnPositions.get(i), hb); } if (isFirstLine() || !linesPrevVisible) { g2d.drawLine(0, ht, stream.contentWidth, ht); linesPrevVisible = true; } g2d.drawLine(stream.contentWidth, ht, stream.contentWidth, hb); g2d.drawLine(0, hb, stream.contentWidth, hb); } currentLine++; page.currentLine++; page.availableLines--; page.currentPixel += getLineHeight(); } else { PositionedRow row = _getRow2(line); if (stream.contentHeight-page.currentPixel < row.reservedSpace) { stream.nextPage(); page = getCurrentPage(); g2d = page.g2d; writeHeader(); row = _getRow2(line); } row.render(g2d); currentLine+= row.realLines; page.currentLine+= row.realLines; page.currentPixel += row.reservedSpace; page.estimateAvailableLines(); } stream.checkNextPage(); return new PDFTableRow(); } void writeLine(String line) throws Exception{ writeLine(line, 0); } void writeLine(TextItem line) throws Exception{ writeLine(line, 0); } private void writeHeader() throws Exception{ if (headerVisible) { TextSize s = currentTextSize; setTextSize(TextSize.MEDIUM); if (title != null) { boolean b = linesVisible; setLinesVisible(false); writeLine(title); setLinesVisible(b); } _writeRow(columnNames); setTextSize(s); } } void writeLine(String line, int x) throws Exception{ int h = getTextHeight(); int ht = getTopHeight(); int hb = getBottomHeight(); PDFPage page = getCurrentPage(); Graphics2D g2d = page.g2d; g2d.drawString(line, x+textOffsetX, h); if (linesVisible) { if (isFirstLine() || !linesPrevVisible) { g2d.drawLine(0, ht, stream.contentWidth, ht); linesPrevVisible = true; } g2d.drawLine(0, ht, 0, hb); g2d.drawLine(stream.contentWidth, ht, stream.contentWidth, hb); g2d.drawLine(0, hb, stream.contentWidth, hb); } currentLine++; page.currentLine++; page.availableLines--; page.currentPixel += getLineHeight(); stream.checkNextPage(); } void writeLine(TextItem line, int x) throws Exception{ int h = getTextHeight(); int ht = getTopHeight(); int hb = getBottomHeight(); PDFPage page = getCurrentPage(); Graphics2D g2d = page.g2d; g2d.drawString(line.getText(), x+textOffsetX, h); if (linesVisible) { if (isFirstLine() || !linesPrevVisible) { g2d.drawLine(0, ht, stream.contentWidth, ht); linesPrevVisible = true; } g2d.drawLine(0, ht, 0, hb); g2d.drawLine(stream.contentWidth, ht, stream.contentWidth, hb); g2d.drawLine(0, hb, stream.contentWidth, hb); } if (line instanceof URLItem) { URL url = ((URLItem)line).getURL(); if (url != null) { addLink(url, 0,ht,stream.contentWidth,hb-ht); } } currentLine++; page.currentLine++; page.availableLines--; page.currentPixel += getLineHeight(); stream.checkNextPage(); } int getTopHeight() { return getTopHeight(currentLine); } int getTopHeight(int line) { return (line-currentLine)*getLineHeight()+getCurrentPage().currentPixel; } int getTextHeight() { PDFPage page = getCurrentPage(); return page.currentPixel+getLineHeight()-page.fm.getDescent()-textOffsetY; } PDFPage getCurrentPage() { return stream.getCurrentPage(); } int getBottomHeight() { return getBottomHeight(currentLine); } protected int getLineHeight() { return getCurrentPage().fm.getHeight()+textOffsetY; } private int getBottomHeight(int line) { return (line-currentLine+1)*getLineHeight()+getCurrentPage().currentPixel; } public int getAvailableLines() { PDFPage page = getCurrentPage(); int contentHeight = stream.contentHeight; int pixelY = page.currentPixel; return (int)Math.floor((contentHeight-pixelY)/getLineHeight()); } private TextSize currentTextSize = TextSize.SMALL; @Override public void setTextSize(TextSize size) { stream.getCurrentPage().setFont(writer.fonts.get(size)); currentTextSize = size; } @Override public TextSize getTextSize() { return currentTextSize; } private PositionedRow _getRow(List line) { PositionedRow row = new PositionedRow(); int h = getTextHeight(); int realLines = 1; int reservedSpace = 0; List> cells = new ArrayList>(line.size()); if (line.size() > 1) { for (int i = 0; i < line.size(); i++) { String text = line.get(i); int availableSize = columnSizes.get(i)-textOffsetX; if (text != null && text.length() > 0) { List pt = getText(text, columnPositions.get(i)+textOffsetX, h,availableSize, columns.get(i).getAlignment()); cells.add(pt); reservedSpace = Math.max(reservedSpace, getResevedSpace(pt)+getLineHeight()); realLines = Math.max(realLines, getLineSpace(pt)); } else { cells.add(Collections. emptyList()); reservedSpace = Math.max(reservedSpace, getLineHeight()); } } } else { String text = line.get(0); int availableSize = stream.contentWidth; if (text != null && text.length() > 0) { List pt = getText(text, textOffsetX, h,availableSize, columns.get(0).getAlignment()); cells.add(pt); reservedSpace = Math.max(reservedSpace, getResevedSpace(pt)+getLineHeight()); realLines = Math.max(realLines, getLineSpace(pt)); } else { cells.add(Collections. emptyList()); reservedSpace = Math.max(reservedSpace, getLineHeight()); } } row.reservedSpace = reservedSpace; row.startLine = currentLine; row.realLines = realLines; row.cells = cells; return row; } private PositionedRow _getRow2(List line) { PositionedRow row = new PositionedRow(); int h = getTextHeight(); int realLines = 1; int reservedSpace = 0; row.cells = new ArrayList>(line.size()); row.urls = new ArrayList(); if (line.size() > 1) { for (int i = 0; i < line.size(); i++) { TextItem item =line.get(i); int availableSize = columnSizes.get(i)-textOffsetX; if (item != null && item.getText().length() > 0) { String text = item.getText(); List pt = getText(text, columnPositions.get(i)+textOffsetX, h,availableSize, columns.get(i).getAlignment()); row.cells.add(pt); reservedSpace = Math.max(reservedSpace, getResevedSpace(pt)+getLineHeight()); realLines = Math.max(realLines, getLineSpace(pt)); } else { row.cells.add(Collections. emptyList()); reservedSpace = Math.max(reservedSpace, getLineHeight()); } if (item instanceof URLItem) { row.urls.add(((URLItem)item).getURL()); } else { row.urls.add(null); } } } else { String text = line.get(0).getText(); int availableSize = stream.contentWidth; if (text != null && text.length() > 0) { List pt = getText(text, textOffsetX, h,availableSize, columns.get(0).getAlignment()); row.cells.add(pt); reservedSpace = Math.max(reservedSpace, getResevedSpace(pt)+getLineHeight()); realLines = Math.max(realLines, getLineSpace(pt)); } else { row.cells.add(Collections. emptyList()); reservedSpace = Math.max(reservedSpace, getLineHeight()); } } row.reservedSpace = reservedSpace; row.startLine = currentLine; row.realLines = realLines; return row; } private int getResevedSpace(List pt) { float sy = pt.get(0).drawPosY; float ey = pt.get(pt.size()-1).drawPosY; return (int)Math.ceil(ey-sy); } /** * Usually lines of multi-line cells consume less space than the maximum line height (FontMetrics). This method calculates the exact amount of lines required by a cell. * @param pt * @return */ private int getLineSpace(List pt) { if (pt.size() < 2) return 1; return (int)(getResevedSpace(pt)/getLineHeight())+1; } private class PositionedRow { int startLine; int realLines; int reservedSpace; List> cells; List urls; void render(Graphics2D g) { int ht = getTopHeight(startLine); int hb = ht +reservedSpace; if (cells.size() > 0) { for (int i = 0; i < cells.size(); i++) { List ptl = cells.get(i); for (PositionedText pt : ptl) pt.render(g); if (urls != null) { URL url = urls.get(i); if (url != null) { addLink(url, columnPositions.get(i),ht,columnSizes.get(i),hb-ht); } } } } if (linesVisible) { if (cells.size() > 0) { for (int i = 0; i < cells.size(); i++) { g.drawLine(columnPositions.get(i), ht, columnPositions.get(i), hb); } } else { g.drawLine(columnPositions.get(0), ht, columnPositions.get(0), hb); } if (isFirstLine() || !linesPrevVisible) { g.drawLine(0, ht, stream.contentWidth, ht); linesPrevVisible = true; } g.drawLine(stream.contentWidth, ht, stream.contentWidth, hb); g.drawLine(0, hb, stream.contentWidth, hb); } } } private void addLink(URL url, int x, int y, int w, int h) { PDFPage page = getCurrentPage(); float fx = +page.stream.marginLeft + x; float fy = -page.stream.marginTop + page.template.getHeight() - y; page.template.setAction(new PdfAction(url), fx, fy-h, fx+w, fy); } private List getText(String text, int x, int y, int cellWidth, Alignment alignment) { List result = new ArrayList(); Hashtable map = new Hashtable(); PDFPage page = getCurrentPage(); Font font = page.getFont(); for (TextAttribute a : font.getAttributes().keySet()) { Object v = font.getAttributes().get(a); if (v != null) map.put(a, v); } // map.putAll(font.getAttributes()); map.put(TextAttribute.FOREGROUND, Color.black); AttributedString attributedText = new AttributedString( text, map); AttributedCharacterIterator paragraph = attributedText.getIterator(); int paragraphStart = paragraph.getBeginIndex(); int paragraphEnd = paragraph.getEndIndex(); LineBreakMeasurer lineMeasurer = new LineBreakMeasurer(paragraph, page.frc); lineMeasurer.setPosition(paragraphStart); // Get lines until the entire paragraph has been displayed. int next, limit, charat, position = 0; int drawPosY = y; while ((position = lineMeasurer.getPosition()) < paragraphEnd) { // Find possible line break and set it as a limit to the next layout next = lineMeasurer.nextOffset(cellWidth); limit = next; charat = text.indexOf(System.getProperty("line.separator"),position+1); //$NON-NLS-1$ if(charat < next && charat != -1){ limit = charat; } // Retrieve next layout. A cleverer program would also cache // these layouts until the component is re-sized. TextLayout layout = lineMeasurer.nextLayout(cellWidth, limit, false); // Compute pen x position. If the paragraph is right-to-left we // will align the TextLayouts to the right edge of the panel. // Note: this won't occur for the English text in this sample. // Note: drawPosX is always where the LEFT of the text is placed. float drawPosX = 0; switch (alignment) { case LEFT: drawPosX = layout.isLeftToRight() ? 0 : cellWidth - layout.getAdvance(); break; case CENTER: drawPosX = (cellWidth - layout.getAdvance()) / 2; break; case RIGHT: drawPosX = layout.isLeftToRight() ? cellWidth - layout.getAdvance() : 0; break; } drawPosX += x; // If text has been forced to vertical, align it to center // if(breakWidth < textAreaWidth) { // float centerCorrection = layout.isLeftToRight() ? // (float) (layout.getAdvance() / 2) : // -1 * (float) (layout.getAdvance() / 2); // drawPosX = textAreaWidth / 2 - centerCorrection; // } // Stop drawing if the text won't fit // if (breakHeight < drawPosY + layout.getDescent() + layout.getLeading()) { // break; // } // drawPosY += layout.getAscent(); // Add TextLayout at (drawPosX, drawPosY). result.add(new PositionedText(drawPosX, drawPosY, layout)); // Move y-coordinate in preparation for next layout. //drawPosY += layout.getDescent() + layout.getLeading(); drawPosY += layout.getDescent() + layout.getLeading() + layout.getAscent(); // drawPosY += getLineHeight(); } return result; } class PositionedText { float drawPosX; float drawPosY; TextLayout layout; public PositionedText(float drawPosX, float drawPosY, TextLayout layout) { this.drawPosX = drawPosX; this.drawPosY = drawPosY; this.layout = layout; } public void render(Graphics2D g) { layout.draw(g, drawPosX, drawPosY); } } }