/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.map;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Icon;
import org.geoserver.platform.Operation;
import org.geoserver.platform.ServiceException;
import org.geoserver.wms.DefaultWebMapService;
import org.geoserver.wms.WMS;
import org.geoserver.wms.WMSMapContent;
import org.geoserver.wms.decoration.MapDecorationLayout;
import org.geoserver.wms.map.PDFMapOutputFormat.PDFMap;
import org.geotools.geometry.jts.Decimator;
import org.geotools.geometry.jts.LiteShape2;
import org.geotools.referencing.operation.transform.AffineTransform2D;
import org.geotools.renderer.lite.LabelCache;
import org.geotools.renderer.lite.ParallelLinesFiller;
import org.geotools.renderer.lite.StreamingRenderer;
import org.geotools.renderer.lite.StyledShapePainter;
import org.geotools.renderer.style.IconStyle2D;
import org.geotools.renderer.style.MarkStyle2D;
import org.geotools.renderer.style.Style2D;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.operation.TransformException;
import org.springframework.util.Assert;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.FontFactory;
import com.lowagie.text.pdf.DefaultFontMapper;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfGraphics2D;
import com.lowagie.text.pdf.PdfPatternPainter;
import com.lowagie.text.pdf.PdfTemplate;
import com.lowagie.text.pdf.PdfWriter;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
/**
* Handles a GetMap request that spects a map in PDF format.
*
* @author Pierre-Emmanuel Balageas, ALCER (http://www.alcer.com)
* @author Simone Giannecchini - GeoSolutions
* @author Gabriel Roldan
* @version $Id$
*/
public class PDFMapResponse extends AbstractMapResponse {
/** A logger for this class. */
private static final Logger LOGGER = org.geotools.util.logging.Logging
.getLogger("org.vfny.geoserver.responses.wms.map.pdf");
/**
* Whether to apply the new vector hatch fill optimization, or not (on by default, this is just a safeguard)
*/
static boolean ENCODE_TILING_PATTERNS = Boolean.parseBoolean(System.getProperty("org.geoserver.pdf.encodeTilingPatterns", "true"));
/**
* A kilobyte
*/
private static final int KB = 1024;
private WMS wms;
public PDFMapResponse(WMS wms) {
super(PDFMap.class, PDFMapOutputFormat.MIME_TYPE);
this.wms = wms;
}
/**
* Writes the PDF.
* <p>
* NOTE: the document seems to actually be created in memory, and being written down to
* {@code output} once we call {@link Document#close()}. If there's no other way to do so, it'd
* be better to actually split out the process into produceMap/write?
* </p>
*
* @see org.geoserver.ows.Response#write(java.lang.Object, java.io.OutputStream,
* org.geoserver.platform.Operation)
*/
@Override
public void write(Object value, OutputStream output, Operation operation) throws IOException,
ServiceException {
Assert.isInstanceOf(PDFMap.class, value);
WMSMapContent mapContent = ((PDFMap) value).getContext();
final int width = mapContent.getMapWidth();
final int height = mapContent.getMapHeight();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("setting up " + width + "x" + height + " image");
}
try {
// step 1: creation of a document-object
// width of document-object is width*72 inches
// height of document-object is height*72 inches
com.lowagie.text.Rectangle pageSize = new com.lowagie.text.Rectangle(width, height);
Document document = new Document(pageSize);
document.setMargins(0, 0, 0, 0);
// step 2: creation of the writer
PdfWriter writer = PdfWriter.getInstance(document, output);
// step 3: we open the document
document.open();
// step 4: we grab the ContentByte and do some stuff with it
// we create a fontMapper and read all the fonts in the font
// directory
DefaultFontMapper mapper = new DefaultFontMapper();
FontFactory.registerDirectories();
// we create a template and a Graphics2D object that corresponds
// with it
PdfContentByte cb = writer.getDirectContent();
PdfTemplate tp = cb.createTemplate(width, height);
PdfGraphics2D graphic = (PdfGraphics2D) tp.createGraphics(width, height, mapper);
// we set graphics options
if (!mapContent.isTransparent()) {
graphic.setColor(mapContent.getBgColor());
graphic.fillRect(0, 0, width, height);
} else {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("setting to transparent");
}
int type = AlphaComposite.SRC;
graphic.setComposite(AlphaComposite.getInstance(type));
Color c = new Color(mapContent.getBgColor().getRed(), mapContent.getBgColor()
.getGreen(), mapContent.getBgColor().getBlue(), 0);
graphic.setBackground(mapContent.getBgColor());
graphic.setColor(c);
graphic.fillRect(0, 0, width, height);
type = AlphaComposite.SRC_OVER;
graphic.setComposite(AlphaComposite.getInstance(type));
}
Rectangle paintArea = new Rectangle(width, height);
StreamingRenderer renderer;
if(ENCODE_TILING_PATTERNS) {
renderer = new PDFStreamingRenderer();
} else {
renderer = new StreamingRenderer();
}
renderer.setMapContent(mapContent);
// TODO: expose the generalization distance as a param
// ((StreamingRenderer) renderer).setGeneralizationDistance(0);
RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
renderer.setJava2DHints(hints);
// we already do everything that the optimized data loading does...
// if we set it to true then it does it all twice...
java.util.Map rendererParams = new HashMap();
rendererParams.put("optimizedDataLoadingEnabled", new Boolean(true));
rendererParams.put("renderingBuffer", new Integer(mapContent.getBuffer()));
// we need the renderer to draw everything on the batik provided graphics object
rendererParams.put(StreamingRenderer.OPTIMIZE_FTS_RENDERING_KEY, Boolean.FALSE);
// render everything in vector form if possible
rendererParams.put(StreamingRenderer.VECTOR_RENDERING_KEY, Boolean.TRUE);
if (DefaultWebMapService.isLineWidthOptimizationEnabled()) {
rendererParams.put(StreamingRenderer.LINE_WIDTH_OPTIMIZATION_KEY, true);
}
rendererParams.put(StreamingRenderer.SCALE_COMPUTATION_METHOD_KEY,
mapContent.getRendererScaleMethod());
renderer.setRendererHints(rendererParams);
Envelope dataArea = mapContent.getRenderingArea();
// enforce no more than x rendering errors
int maxErrors = wms.getMaxRenderingErrors();
MaxErrorEnforcer errorChecker = new MaxErrorEnforcer(renderer, maxErrors);
// Add a render listener that ignores well known rendering exceptions and reports back
// non
// ignorable ones
final RenderExceptionStrategy nonIgnorableExceptionListener;
nonIgnorableExceptionListener = new RenderExceptionStrategy(renderer);
renderer.addRenderListener(nonIgnorableExceptionListener);
// enforce max memory usage
int maxMemory = wms.getMaxRequestMemory() * KB;
PDFMaxSizeEnforcer memoryChecker = new PDFMaxSizeEnforcer(renderer, graphic, maxMemory);
// render the map
renderer.paint(graphic, paintArea, mapContent.getRenderingArea(),
mapContent.getRenderingTransform());
// render the watermark
MapDecorationLayout.Block watermark = RenderedImageMapOutputFormat.getWatermark(wms
.getServiceInfo());
if (watermark != null) {
MapDecorationLayout layout = new MapDecorationLayout();
layout.paint(graphic, paintArea, mapContent);
}
// check if a non ignorable error occurred
if (nonIgnorableExceptionListener.exceptionOccurred()) {
Exception renderError = nonIgnorableExceptionListener.getException();
throw new ServiceException("Rendering process failed", renderError, "internalError");
}
// check if too many errors occurred
if (errorChecker.exceedsMaxErrors()) {
throw new ServiceException("More than " + maxErrors
+ " rendering errors occurred, bailing out",
errorChecker.getLastException(), "internalError");
}
// check we did not use too much memory
if (memoryChecker.exceedsMaxSize()) {
long kbMax = maxMemory / KB;
throw new ServiceException(
"Rendering request used more memory than the maximum allowed:" + kbMax
+ "KB");
}
graphic.dispose();
cb.addTemplate(tp, 0, 0);
// step 5: we close the document
document.close();
writer.flush();
writer.close();
} catch (DocumentException t) {
throw new ServiceException("Error setting up the PDF", t, "internalError");
}
}
private static class PDFStreamingRenderer extends StreamingRenderer {
public PDFStreamingRenderer() {
this.painter = new PDFStyledPainter(labelCache);
}
}
/**
* Optimized StyledShapePainter that can optimize painting repeated external graphics
* by encoding them in a native PDF pattern painter, with no repetition
*/
private static class PDFStyledPainter extends StyledShapePainter {
/**
* Re-creating the patterns is expensive and would increase the overall
* document size, keep a cache of them instead
*/
WeakHashMap<Style2D, PdfPatternPainter> patternCache = new WeakHashMap<>();
public PDFStyledPainter(LabelCache labelCache) {
super(labelCache);
}
@Override
protected void paintGraphicFill(Graphics2D graphics, Shape shape, Style2D graphicFill,
double scale) {
if(graphics instanceof PdfGraphics2D && (graphicFill instanceof IconStyle2D || isMarkNonHatchFill(graphicFill))) {
fillShapeAsPattern((PdfGraphics2D) graphics, shape, graphicFill, scale);
} else {
super.paintGraphicFill(graphics, shape, graphicFill, scale);
}
}
private boolean isMarkNonHatchFill(Style2D graphicFill) {
if(!(graphicFill instanceof MarkStyle2D)) {
return false;
}
if(OPTIMIZE_VECTOR_HATCH_FILLS) {
MarkStyle2D ms = (MarkStyle2D) graphicFill;
ParallelLinesFiller filler = ParallelLinesFiller.fromStipple(ms.getShape());
if(filler != null) {
return false;
}
}
return true;
}
private void fillShapeAsPattern(PdfGraphics2D graphics, Shape shape, Style2D graphicFill, double scale) {
final PdfContentByte content = graphics.getContent();
Paint oldPaint = graphics.getPaint();
try {
// prepare the PDF pattern
PdfPatternPainter pattern = getPatternPainter(graphicFill, content, scale);
final PdfContentByte cb = graphics.getContent();
content.setPatternFill(pattern);
// Paint the pattern. Unfortunately we cannot rely on PDFGraphic but have to
// use the low level API to walk the shape
AffineTransform tx = new AffineTransform(graphics.getTransform());
float documentHeight = cb.getPdfDocument().getPageSize().getHeight();
tx.concatenate(new AffineTransform(1, 0, 0, -1, 0, documentHeight));
PathIterator pathIterator = shape.getPathIterator(tx);
float[] coords = new float[6];
if (!pathIterator.isDone()) {
// walk the path
while (!pathIterator.isDone()) {
int segtype = pathIterator.currentSegment(coords);
switch (segtype) {
case PathIterator.SEG_MOVETO:
cb.moveTo(coords[0], coords[1]);
break;
case PathIterator.SEG_LINETO:
cb.lineTo(coords[0], coords[1]);
break;
case PathIterator.SEG_QUADTO:
cb.curveTo(coords[0], coords[1], coords[2], coords[3]);
break;
case PathIterator.SEG_CUBICTO:
cb.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4],
coords[5]);
break;
case PathIterator.SEG_CLOSE:
cb.closePath();
break;
}
pathIterator.next();
}
// run the actual fill
if (pathIterator.getWindingRule() == PathIterator.WIND_EVEN_ODD) {
cb.eoFill();
} else {
cb.fill();
}
}
} finally {
// reset the old paint
graphics.setPaint(oldPaint);
}
}
private PdfPatternPainter getPatternPainter(Style2D graphicFill,
final PdfContentByte content, double scale) {
PdfPatternPainter pattern = patternCache.get(graphicFill);
if (pattern == null) {
pattern = buildPattern(graphicFill, content, scale);
patternCache.put(graphicFill, pattern);
}
return pattern;
}
private PdfPatternPainter buildPattern(Style2D graphicFill, final PdfContentByte content, double scale) {
PdfPatternPainter pattern;
if(graphicFill instanceof IconStyle2D) {
final Icon icon = ((IconStyle2D) graphicFill).getIcon();
int width = icon.getIconWidth();
int height = icon.getIconHeight();
pattern = content.createPattern(width, height);
Graphics2D patternGraphic = pattern.createGraphics(width, height);
icon.paintIcon(null, patternGraphic, 0, 0);
patternGraphic.dispose();
} else if(graphicFill instanceof MarkStyle2D) {
MarkStyle2D mark = (MarkStyle2D) graphicFill;
int size = (int) Math.round(mark.getSize());
pattern = content.createPattern(size, size);
Graphics2D patternGraphic = pattern.createGraphics(size, size);
GeometryFactory geomFactory = new GeometryFactory();
Coordinate stippleCoord = new Coordinate(size / 2d, size / 2d);
Geometry stipplePoint = geomFactory.createPoint(stippleCoord);
Decimator nullDecimator = new Decimator(-1, -1);
AffineTransform2D identityTransf = new AffineTransform2D(new AffineTransform());
try {
paint(patternGraphic, new LiteShape2(stipplePoint, identityTransf, nullDecimator, false), mark, scale);
} catch(TransformException | FactoryException e) {
// this should not happen given the arguments passed to the lite shape
throw new RuntimeException(e);
}
} else {
throw new IllegalArgumentException("Unsupported style " + graphicFill);
}
return pattern;
}
}
}