/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2011 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.plugin.printing.component;
import java.awt.Color;
import java.awt.Font;
import java.net.URL;
import java.util.Stack;
import org.geomajas.configuration.SymbolInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.lowagie.text.BadElementException;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Image;
import com.lowagie.text.Rectangle;
import com.lowagie.text.Utilities;
import com.lowagie.text.pdf.BaseFont;
import com.lowagie.text.pdf.DefaultFontMapper;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfGState;
import com.lowagie.text.pdf.PdfTemplate;
import com.lowagie.text.pdf.PdfWriter;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.LineString;
import com.vividsolutions.jts.geom.MultiLineString;
import com.vividsolutions.jts.geom.MultiPoint;
import com.vividsolutions.jts.geom.MultiPolygon;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.Polygon;
/**
* Context for PDF printing. This is a wrapper on a PdfWriter on which components of a print template can render
* themselves. Components do not need to know about the template's layout and can draw in a relative coordinate system
* in which their bottom-left origin corresponds to (0,0).
*
* @author Jan De Moerloose
*/
public class PdfContext {
private PdfWriter writer;
private PdfTemplate template;
// current origin
private float origX;
private float origY;
// stack of previous origins
private Stack<Float> prevOrigX = new Stack<Float>();
private Stack<Float> prevOrigY = new Stack<Float>();
private final Logger log = LoggerFactory.getLogger(PdfContext.class);
/**
* Constructs a context for the specified writer and application.
*
* @param writer writer
*/
public PdfContext(PdfWriter writer) {
this.writer = writer;
}
/**
* Initializes context size.
*
* @param rectangle rectangle
*/
public void initSize(Rectangle rectangle) {
template = writer.getDirectContent().createTemplate(rectangle.getWidth(), rectangle.getHeight());
}
public void setOrigin(float x, float y) {
this.origX = x;
this.origY = y;
}
/**
* Return the text box for the specified text and font.
*
* @param text text
* @param font font
* @return text box
*/
public Rectangle getTextSize(String text, Font font) {
template.saveState();
// get the font
DefaultFontMapper mapper = new DefaultFontMapper();
BaseFont bf = mapper.awtToPdf(font);
template.setFontAndSize(bf, font.getSize());
// calculate text width and height
float textWidth = template.getEffectiveStringWidth(text, false);
float ascent = bf.getAscentPoint(text, font.getSize());
float descent = bf.getDescentPoint(text, font.getSize());
float textHeight = ascent - descent;
template.restoreState();
return new Rectangle(0, 0, textWidth, textHeight);
}
/**
* Draw text in the center of the specified box.
*
* @param text text
* @param font font
* @param box box to put text int
* @param fontColor colour
*/
public void drawText(String text, Font font, Rectangle box, Color fontColor) {
template.saveState();
// get the font
DefaultFontMapper mapper = new DefaultFontMapper();
BaseFont bf = mapper.awtToPdf(font);
template.setFontAndSize(bf, font.getSize());
// calculate descent
float descent = 0;
if (text != null) {
descent = bf.getDescentPoint(text, font.getSize());
}
// calculate the fitting size
Rectangle fit = getTextSize(text, font);
// draw text if necessary
template.setColorFill(fontColor);
template.beginText();
template.showTextAligned(PdfContentByte.ALIGN_LEFT, text, origX + box.getLeft() + 0.5f
* (box.getWidth() - fit.getWidth()), origY + box.getBottom() + 0.5f
* (box.getHeight() - fit.getHeight()) - descent, 0);
template.endText();
template.restoreState();
}
/**
* Draw a rectangular boundary.
*
* @param rect rectangle
*/
public void strokeRectangle(Rectangle rect) {
strokeRectangle(rect, Color.black, 1, null);
}
/**
* Draw a rectangular boundary with this color and linewidth.
*
* @param rect
* rectangle
* @param color
* color
* @param linewidth
* line width
*/
public void strokeRectangle(Rectangle rect, Color color, float linewidth) {
strokeRectangle(rect, color, linewidth, null);
}
public void strokeRectangle(Rectangle rect, Color color, float linewidth, float[] dashArray) {
template.saveState();
setStroke(color, linewidth, dashArray);
template.rectangle(origX + rect.getLeft(), origY + rect.getBottom(), rect.getWidth(), rect.getHeight());
template.stroke();
template.restoreState();
}
/**
* Draw a rounded rectangular boundary.
*
* @param rect rectangle
* @param color colour
* @param linewidth line width
* @param r radius for rounded corners
*/
public void strokeRoundRectangle(Rectangle rect, Color color, float linewidth, float r) {
template.saveState();
setStroke(color, linewidth, null);
template.roundRectangle(origX + rect.getLeft(), origY + rect.getBottom(), rect.getWidth(), rect.getHeight(), r);
template.stroke();
template.restoreState();
}
/**
* Draw a rectangle's interior.
*
* @param rect rectangle
*/
public void fillRectangle(Rectangle rect) {
fillRectangle(rect, Color.white);
}
/**
* Draw a rectangle's interior with this color.
*
* @param rect rectangle
* @param color colour
*/
public void fillRectangle(Rectangle rect, Color color) {
template.saveState();
setFill(color);
template.rectangle(origX + rect.getLeft(), origY + rect.getBottom(), rect.getWidth(), rect.getHeight());
template.fill();
template.restoreState();
}
public void fillRoundRectangle(Rectangle rect, Color color, float r) {
template.saveState();
setFill(color);
template.roundRectangle(origX + rect.getLeft(), origY + rect.getBottom(), rect.getWidth(), rect.getHeight(), r);
template.fill();
template.restoreState();
}
/**
* Draw an elliptical exterior with this color.
*
* @param rect rectangle in which ellipse should fit
* @param color colour to use for stroking
* @param linewidth line width
*/
public void strokeEllipse(Rectangle rect, Color color, float linewidth) {
template.saveState();
setStroke(color, linewidth, null);
template.ellipse(origX + rect.getLeft(), origY + rect.getBottom(), origX + rect.getRight(),
origY + rect.getTop());
template.stroke();
template.restoreState();
}
/**
* Draw an elliptical interior with this color.
*
* @param rect rectangle in which ellipse should fit
* @param color colour to use for filling
*/
public void fillEllipse(Rectangle rect, Color color) {
template.saveState();
setFill(color);
template.ellipse(origX + rect.getLeft(), origY + rect.getBottom(), origX + rect.getRight(),
origY + rect.getTop());
template.fill();
template.restoreState();
}
/**
* Move this rectangle to the specified bottom-left point.
*
* @param rect rectangle to move
* @param x new x origin
* @param y new y origin
*/
public void moveRectangleTo(Rectangle rect, float x, float y) {
float width = rect.getWidth();
float height = rect.getHeight();
rect.setLeft(x);
rect.setBottom(y);
rect.setRight(rect.getLeft() + width);
rect.setTop(rect.getBottom() + height);
}
/**
* Translate this rectangle over the specified following distances.
*
* @param rect rectangle to move
* @param dx delta x
* @param dy delta y
*/
public void translateRectangle(Rectangle rect, float dx, float dy) {
float width = rect.getWidth();
float height = rect.getHeight();
rect.setLeft(rect.getLeft() + dx);
rect.setBottom(rect.getBottom() + dy);
rect.setRight(rect.getLeft() + dx + width);
rect.setTop(rect.getBottom() + dy + height);
}
public Color getColor(String css, float opacity) {
return getColor(css, opacity, Color.white);
}
public Color getColor(String css, float opacity, Color defaultColor) {
Color color;
if (null == css) {
color = defaultColor;
} else {
color = Color.decode(css);
}
return new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (opacity * 255));
}
public float[] getDashArray(String dashArrayString) {
if (dashArrayString == null || "".equals(dashArrayString.trim()) || "none".equals(dashArrayString)) {
return null;
}
try {
String[] res = dashArrayString.split(",");
float[] dasharr = new float[res.length];
for (int i = 0; i < res.length; i++) {
dasharr[i] = Float.parseFloat(res[i]);
}
return dasharr;
} catch (Exception e) {
log.warn("Error in dashArrayString: " + dashArrayString);
return null;
}
}
public Image getImage(String path) {
try {
URL url = getClass().getResource(path);
if (url == null) {
url = Utilities.toURL(path);
}
return Image.getInstance(url);
} catch (Exception e) {
log.warn("could not fetch image", e);
return null;
}
}
/**
* Draws the specified image with the first rectangle's bounds, clipping with the second one and adding
* transparency.
*
* @param img image
* @param rect rectangle
* @param clipRect clipping bounds
*/
public void drawImage(Image img, Rectangle rect, Rectangle clipRect) {
drawImage(img, rect, clipRect, 1);
}
/**
* Draws the specified image with the first rectangle's bounds, clipping with the second one.
*
* @param img image
* @param rect rectangle
* @param clipRect clipping bounds
* @param opacity opacity of the image (1 = opaque, 0= transparent)
*/
public void drawImage(Image img, Rectangle rect, Rectangle clipRect, float opacity) {
try {
template.saveState();
// opacity
PdfGState state = new PdfGState();
state.setFillOpacity(opacity);
state.setBlendMode(PdfGState.BM_NORMAL);
template.setGState(state);
// clipping code
if (clipRect != null) {
template.rectangle(clipRect.getLeft() + origX, clipRect.getBottom() + origY, clipRect.getWidth(),
clipRect.getHeight());
template.clip();
template.newPath();
}
template.addImage(img, rect.getWidth(), 0, 0, rect.getHeight(), origX + rect.getLeft(), origY
+ rect.getBottom());
} catch (DocumentException e) {
log.warn("could not draw image", e);
} finally {
template.restoreState();
}
}
/**
* Draw a path specified by relative coordinates in [0,1] range wrt the specified rectangle.
*
* @param x x-ordinate
* @param y y-ordinate
* @param rect rectangle
* @param color color to use
* @param lineWidth line width
* @param dashArray lengths for dashed and white area
*/
public void drawRelativePath(float[] x, float[] y, Rectangle rect, Color color, float lineWidth,
float[] dashArray) {
template.saveState();
setStroke(color, lineWidth, dashArray);
template.moveTo(origX + getAbsoluteX(x[0], rect), origY + getAbsoluteY(y[0], rect));
for (int i = 1; i < x.length; i++) {
template.lineTo(origX + getAbsoluteX(x[i], rect), origY + getAbsoluteY(y[i], rect));
}
template.stroke();
template.restoreState();
}
private float getAbsoluteX(float f, Rectangle rect) {
return rect.getLeft() + f * rect.getWidth();
}
private float getAbsoluteY(float f, Rectangle rect) {
return rect.getBottom() + f * rect.getHeight();
}
private void setStroke(Color color, float linewidth, float[] dashArray) {
// Color and transparency
PdfGState state = new PdfGState();
state.setStrokeOpacity(color.getAlpha());
state.setBlendMode(PdfGState.BM_NORMAL);
template.setGState(state);
template.setColorStroke(color);
// linewidth
template.setLineWidth(linewidth);
if (dashArray != null) {
template.setLineDash(dashArray, 0f);
}
}
private void setFill(Color color) {
// Color and transparency
PdfGState state = new PdfGState();
state.setFillOpacity(color.getAlpha() / 255f);
state.setBlendMode(PdfGState.BM_NORMAL);
template.setGState(state);
template.setColorFill(color);
}
/**
* Draw the specified geometry.
*
* @param geometry geometry to draw
* @param symbol symbol for geometry
* @param fillColor fill colour
* @param strokeColor stroke colour
* @param lineWidth line width
* @param clipRect clipping rectangle
*/
public void drawGeometry(Geometry geometry, SymbolInfo symbol, Color fillColor, Color strokeColor, float lineWidth,
float[] dashArray, Rectangle clipRect) {
template.saveState();
// clipping code
if (clipRect != null) {
template.rectangle(clipRect.getLeft() + origX, clipRect.getBottom() + origY, clipRect.getWidth(), clipRect
.getHeight());
template.clip();
template.newPath();
}
setStroke(strokeColor, lineWidth, dashArray);
setFill(fillColor);
drawGeometry(geometry, symbol);
template.restoreState();
}
private void drawGeometry(Geometry g, SymbolInfo symbol) {
if (g instanceof MultiPolygon) {
MultiPolygon mpoly = (MultiPolygon) g;
for (int i = 0; i < mpoly.getNumGeometries(); i++) {
drawGeometry(mpoly.getGeometryN(i), symbol);
}
} else if (g instanceof MultiLineString) {
MultiLineString mline = (MultiLineString) g;
for (int i = 0; i < mline.getNumGeometries(); i++) {
drawGeometry(mline.getGeometryN(i), symbol);
}
} else if (g instanceof MultiPoint) {
MultiPoint mpoint = (MultiPoint) g;
for (int i = 0; i < mpoint.getNumGeometries(); i++) {
drawGeometry(mpoint.getGeometryN(i), symbol);
}
} else if (g instanceof Polygon) {
Polygon poly = (Polygon) g;
LineString shell = poly.getExteriorRing();
int nHoles = poly.getNumInteriorRing();
drawPathContent(shell.getCoordinates());
for (int j = 0; j < nHoles; j++) {
drawPathContent(poly.getInteriorRingN(j).getCoordinates());
}
template.closePathEoFillStroke();
} else if (g instanceof LineString) {
LineString line = (LineString) g;
drawPathContent(line.getCoordinates());
template.stroke();
} else if (g instanceof Point) {
Point point = (Point) g;
drawPoint(point.getCoordinate(), symbol);
template.fillStroke();
}
}
private void drawPathContent(Coordinate[] coord) {
template.moveTo(origX + (float) coord[0].x, origY + (float) coord[0].y);
for (int i = 1; i < coord.length; i++) {
template.lineTo(origX + (float) coord[i].x, origY + (float) coord[i].y);
}
}
private void drawPoint(Coordinate coord, SymbolInfo symbol) {
if (symbol.getImage() != null) {
try {
Image pointImage = Image.getInstance(symbol.getImage().getHref());
//template.addImage(image, width, 0, 0, height, x, y)
template.addImage(pointImage, symbol.getImage().getWidth(), 0, 0, symbol.getImage().getHeight()
, origX + (float) coord.x, origY + (float) coord.y);
} catch (Exception ex) {
log.error("Not able to create POINT image for rendering", ex);
}
} else if (symbol.getCircle() != null) {
float radius = symbol.getCircle().getR();
template.circle(origX + (float) coord.x, origY + (float) coord.y, radius);
} else if (symbol.getRect() != null) {
float width = symbol.getRect().getW();
float height = symbol.getRect().getW();
template.rectangle(origX + (float) coord.x - (width / 2), origY + (float) coord.y - (height / 2), width,
height);
}
}
/**
* Return this context as an image.
*
* @return this context as image
* @throws BadElementException oops
*/
public Image getImage() throws BadElementException {
return Image.getInstance(template);
}
/**
* Push the current origin on the stack.
*/
public void saveOrigin() {
prevOrigX.push(origX);
prevOrigY.push(origY);
}
/**
* Pop the previous origin from the stack.
*/
public void restoreOrigin() {
origX = prevOrigX.pop();
origY = prevOrigY.pop();
}
public Color makeTransparent(Color color, float alpha) {
return new Color(color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f, alpha);
}
public void drawLine(float x0, float y0, float x1, float y1, Color color, float lineWidth) {
template.saveState();
setStroke(color, lineWidth, null);
template.moveTo(origX + x0, origY + y0);
template.lineTo(origX + x1, origY + y1);
template.stroke();
template.restoreState();
}
/**
* Converts an absolute rectangle to a relative one wrt to the current coordinate system.
*
* @param rect absolute rectangle
* @return relative rectangle
*/
public Rectangle toRelative(Rectangle rect) {
return new Rectangle(rect.getLeft() - origX, rect.getBottom() - origY, rect.getRight() - origX, rect.getTop()
- origY);
}
}