/* (c) 2017 Open Source Geospatial Foundation - all rights reserved
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.wms.map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.awt.geom.Point2D;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.commons.io.IOUtils;
import org.apache.pdfbox.contentstream.PDFGraphicsStreamEngine;
import org.apache.pdfbox.contentstream.PDFStreamEngine;
import org.apache.pdfbox.contentstream.operator.Operator;
import org.apache.pdfbox.contentstream.operator.color.SetNonStrokingColorN;
import org.apache.pdfbox.cos.COSBase;
import org.apache.pdfbox.cos.COSName;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.encryption.InvalidPasswordException;
import org.apache.pdfbox.pdmodel.graphics.color.PDColor;
import org.apache.pdfbox.pdmodel.graphics.color.PDPattern;
import org.apache.pdfbox.pdmodel.graphics.image.PDImage;
import org.apache.pdfbox.pdmodel.graphics.pattern.PDAbstractPattern;
import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern;
import org.geoserver.catalog.Catalog;
import org.geoserver.data.test.MockData;
import org.geoserver.data.test.SystemTestData;
import org.geoserver.wms.WMSTestSupport;
import org.junit.After;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletResponse;
public class PDFGetMapTest extends WMSTestSupport {
String bbox = "-1.5,-0.5,1.5,1.5";
String layers = getLayerId(MockData.BASIC_POLYGONS);
String requestBase = "wms?bbox=" + bbox
+ "&layers=" + layers + "&Format=application/pdf&request=GetMap"
+ "&width=300&height=300&srs=EPSG:4326";
static boolean tilingPatterDefault = PDFMapResponse.ENCODE_TILING_PATTERNS;
@After
public void setupTilingPatternEncoding() {
PDFMapResponse.ENCODE_TILING_PATTERNS = tilingPatterDefault;
}
@Override
protected void onSetUp(SystemTestData testData) throws Exception {
super.onSetUp(testData);
Catalog catalog = getCatalog();
testData.addStyle("burg-fill", "burg-fill.sld", PDFGetMapTest.class, catalog);
testData.addStyle("triangle-fill", "triangle-fill.sld", PDFGetMapTest.class, catalog);
testData.addStyle("hatch-fill", "hatch-fill.sld", PDFGetMapTest.class, catalog);
// copy over the svg icon
File styles = new File(testData.getDataDirectoryRoot(), "styles");
File burg = new File(styles, "burg02.svg");
try(InputStream is = getClass().getResourceAsStream("burg02.svg"); FileOutputStream fos = new FileOutputStream(burg)) {
IOUtils.copy(is, fos);
}
}
@Test
public void testBasicPolygonMap() throws Exception {
MockHttpServletResponse response = getAsServletResponse(requestBase + "&styles=");
assertEquals("application/pdf", response.getContentType());
PDTilingPattern tilingPattern = getTilingPattern(response.getContentAsByteArray());
assertNull(tilingPattern);
}
@Test
public void testSvgFillOptimization() throws Exception {
// get a single polygon to ease testing
MockHttpServletResponse response = getAsServletResponse(requestBase + "&styles=burg-fill&featureId=BasicPolygons.1107531493630");
assertEquals("application/pdf", response.getContentType());
PDTilingPattern tilingPattern = getTilingPattern(response.getContentAsByteArray());
assertNotNull(tilingPattern);
assertEquals(20, tilingPattern.getXStep(), 0d);
assertEquals(20, tilingPattern.getYStep(), 0d);
}
@Test
public void testSvgFillOptimizationDisabled() throws Exception {
PDFMapResponse.ENCODE_TILING_PATTERNS = false;
// get a single polygon to ease testing
MockHttpServletResponse response = getAsServletResponse(requestBase + "&styles=burg-fill&featureId=BasicPolygons.1107531493630");
assertEquals("application/pdf", response.getContentType());
// the tiling pattern encoding has been disabled
PDTilingPattern tilingPattern = getTilingPattern(response.getContentAsByteArray());
assertNull(tilingPattern);
}
@Test
public void testTriangleFillOptimization() throws Exception {
MockHttpServletResponse response = getAsServletResponse(requestBase + "&styles=triangle-fill&featureId=BasicPolygons.1107531493630");
assertEquals("application/pdf", response.getContentType());
File file = new File("./target/test.pdf");
org.apache.commons.io.FileUtils.writeByteArrayToFile(file, response.getContentAsByteArray());
PDTilingPattern tilingPattern = getTilingPattern(response.getContentAsByteArray());
assertNotNull(tilingPattern);
assertEquals(20, tilingPattern.getXStep(), 0d);
assertEquals(20, tilingPattern.getYStep(), 0d);
}
@Test
public void testHatchFillOptimization() throws Exception {
MockHttpServletResponse response = getAsServletResponse(requestBase + "&styles=hatch-fill&featureId=BasicPolygons.1107531493630");
assertEquals("application/pdf", response.getContentType());
// for hatches we keep the existing "set of parallel lines" optimization approach, need to determine
// if we want to remove it or not yet
PDTilingPattern tilingPattern = getTilingPattern(response.getContentAsByteArray());
assertNull(tilingPattern);
}
/**
* Returns the last tiling pattern found during a render of the PDF document. Can be used to extract
* one tiling pattern that gets actually used to render shapes (meant to be used against a document
* that only has a single tiling pattern)
*
* @param pdfDocument
* @return
* @throws InvalidPasswordException
* @throws IOException
*/
PDTilingPattern getTilingPattern(byte[] pdfDocument) throws InvalidPasswordException, IOException {
// load the document using PDFBOX (iText is no good for parsing tiling patterns, mostly works
// well for text and image extraction, spent a few hours trying to use it with no results)
PDDocument doc = PDDocument.load(pdfDocument);
PDPage page = doc.getPage(0);
// use a graphics stream engine, it's the only thing I could find that parses the PDF
// deep enough to allow catching the tiling pattern in parsed form
AtomicReference<PDTilingPattern> pattern = new AtomicReference<>();
PDFStreamEngine engine = new PDFGraphicsStreamEngine(page) {
@Override
public void strokePath() throws IOException {
}
@Override
public void shadingFill(COSName shadingName) throws IOException {
}
@Override
public void moveTo(float x, float y) throws IOException {
}
@Override
public void lineTo(float x, float y) throws IOException {
}
@Override
public Point2D getCurrentPoint() throws IOException {
return null;
}
@Override
public void fillPath(int windingRule) throws IOException {
}
@Override
public void fillAndStrokePath(int windingRule) throws IOException {
}
@Override
public void endPath() throws IOException {
}
@Override
public void drawImage(PDImage pdImage) throws IOException {
}
@Override
public void curveTo(float x1, float y1, float x2, float y2, float x3, float y3)
throws IOException {
}
@Override
public void closePath() throws IOException {
}
@Override
public void clip(int windingRule) throws IOException {
}
@Override
public void appendRectangle(Point2D p0, Point2D p1, Point2D p2, Point2D p3) throws IOException {
}
};
// setup the tiling pattern trap
engine.addOperator(new SetNonStrokingColorN() {
@Override
public void process(Operator operator, List<COSBase> arguments) throws IOException {
super.process(operator, arguments);
PDColor color = context.getGraphicsState().getNonStrokingColor();
if(context.getGraphicsState().getNonStrokingColorSpace() instanceof PDPattern) {
PDPattern colorSpace = (PDPattern) context.getGraphicsState().getNonStrokingColorSpace();
PDAbstractPattern ap = colorSpace.getPattern(color);
if(ap instanceof PDTilingPattern) {
pattern.set((PDTilingPattern) ap);
}
}
}
});
// run it
engine.processPage(page);
return pattern.get();
}
}