package com.revolsys.swing.pdf; import java.awt.Canvas; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.geom.AffineTransform; import java.awt.geom.PathIterator; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import javax.measure.Measure; import javax.measure.quantity.Length; import javax.measure.unit.Unit; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.PDResources; import org.apache.pdfbox.pdmodel.edit.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont; import org.apache.pdfbox.pdmodel.graphics.PDExtendedGraphicsState; import org.apache.pdfbox.pdmodel.graphics.PDLineDashPattern; import com.revolsys.geometry.cs.CoordinateSystem; import com.revolsys.geometry.cs.esri.EsriCoordinateSystems; import com.revolsys.geometry.cs.esri.EsriCsWktWriter; import com.revolsys.geometry.model.BoundingBox; import com.revolsys.geometry.model.Geometry; import com.revolsys.geometry.model.GeometryFactory; import com.revolsys.geometry.model.LineString; import com.revolsys.geometry.model.LinearRing; import com.revolsys.geometry.model.Point; import com.revolsys.geometry.model.Polygon; import com.revolsys.geometry.model.impl.PointDoubleXYOrientation; import com.revolsys.io.BaseCloseable; import com.revolsys.raster.io.format.pdf.PdfUtil; import com.revolsys.record.Record; import com.revolsys.swing.map.Viewport2D; import com.revolsys.swing.map.layer.Project; import com.revolsys.swing.map.layer.record.AbstractRecordLayer; import com.revolsys.swing.map.layer.record.LayerRecord; import com.revolsys.swing.map.layer.record.renderer.AbstractRecordLayerRenderer; import com.revolsys.swing.map.layer.record.renderer.TextStyleRenderer; import com.revolsys.swing.map.layer.record.style.GeometryStyle; import com.revolsys.swing.map.layer.record.style.TextStyle; import com.revolsys.util.Exceptions; import com.revolsys.util.Property; public class PdfViewport extends Viewport2D implements BaseCloseable { private final Set<Float> alphaSet = new HashSet<>(); private final Canvas canvas = new Canvas(); private final PDPageContentStream contentStream; private final PDDocument document; private final Map<String, PDFont> fonts = new HashMap<>(); private final PDPage page; private int styleId = 0; private final Map<GeometryStyle, String> styleNames = new HashMap<>(); public PdfViewport(final PDDocument document, final PDPage page, final Project project, final int width, final int height, final BoundingBox boundingBox) throws IOException { super(project, width, height, boundingBox); this.document = document; this.page = page; this.contentStream = new PDPageContentStream(document, page); final COSDictionary pageDictionary = page.getCOSDictionary(); final COSArray viewports = PdfUtil.getArray(pageDictionary, "VP"); final COSDictionary viewport = new COSDictionary(); viewports.add(viewport); viewport.setName(COSName.TYPE, "Viewport"); final COSArray bbox = PdfUtil.intArray(0, 0, width, height); viewport.setItem(COSName.BBOX, bbox); viewport.setString(COSName.NAME, "Main Map"); final COSDictionary measure = PdfUtil.getDictionary(viewport, "Measure"); measure.setName(COSName.TYPE, "Measure"); measure.setName(COSName.SUBTYPE, "GEO"); final COSArray bounds = PdfUtil.intArray(0, 0, 0, 1, 1, 1, 1, 0); measure.setItem("Bounds", bounds); final GeometryFactory geometryFactory = boundingBox.getGeometryFactory(); final double minX = boundingBox.getMinX(); final double maxX = boundingBox.getMaxX(); final double minY = boundingBox.getMinY(); final double maxY = boundingBox.getMaxY(); final COSArray geoPoints = new COSArray(); addPoint(geoPoints, geometryFactory, minX, minY); addPoint(geoPoints, geometryFactory, minX, maxY); addPoint(geoPoints, geometryFactory, maxX, maxY); addPoint(geoPoints, geometryFactory, maxX, minY); measure.setItem("GPTS", geoPoints); final COSArray lpts = PdfUtil.intArray(0, 0, 0, 1, 1, 1, 1, 0); measure.setItem("LPTS", lpts); final COSDictionary gcs = PdfUtil.getDictionary(measure, "GCS"); if (geometryFactory.isProjected()) { gcs.setName(COSName.TYPE, "PROJCS"); } else { gcs.setName(COSName.TYPE, "GEOGCS"); } final int srid = geometryFactory.getCoordinateSystemId(); gcs.setInt("EPSG", srid); final CoordinateSystem coordinateSystem = geometryFactory.getCoordinateSystem(); final CoordinateSystem esri = EsriCoordinateSystems.getCoordinateSystem(coordinateSystem); String wkt = EsriCsWktWriter.toWkt(esri); wkt = wkt.replaceAll("false_easting", "False_Easting"); wkt = wkt.replaceAll("false_northing", "False_Northing"); wkt = wkt.replaceAll("Popular_Visualisation_Pseudo_Mercator", "Mercator"); gcs.setString("WKT", wkt); } private void addPoint(final COSArray geoPoints, final GeometryFactory geometryFactory, final double x, final double y) { final Point point = geometryFactory.point(x, y); final GeometryFactory geographicGeometryFactory = geometryFactory .getGeographicGeometryFactory(); final Point geoPoint = point.convertGeometry(geographicGeometryFactory); final double lon = geoPoint.getX(); final double lat = geoPoint.getY(); PdfUtil.addFloat(geoPoints, lat); PdfUtil.addFloat(geoPoints, lon); } @Override public void close() { try { this.contentStream.close(); } catch (final IOException e) { throw Exceptions.wrap(e); } } @Override public void drawGeometry(final Geometry geometry, final GeometryStyle style) { try { this.contentStream.saveGraphicsState(); setGeometryStyle(style); this.contentStream.setNonStrokingColor(style.getPolygonFill()); this.contentStream.setStrokingColor(style.getLineColor()); for (Geometry part : geometry.geometries()) { part = part.convertGeometry(getGeometryFactory()); if (part instanceof LineString) { final LineString line = (LineString)part; drawLine(line); this.contentStream.stroke(); } else if (part instanceof Polygon) { final Polygon polygon = (Polygon)part; int i = 0; for (final LinearRing ring : polygon.rings()) { if (i == 0) { if (ring.isClockwise()) { drawLineReverse(ring); } else { drawLine(ring); } } else { if (ring.isCounterClockwise()) { drawLineReverse(ring); } else { drawLine(ring); } } this.contentStream.closeSubPath(); i++; } this.contentStream.fill(PathIterator.WIND_NON_ZERO); for (final LinearRing ring : polygon.rings()) { drawLine(ring); this.contentStream.stroke(); } } } } catch (final IOException e) { } finally { try { this.contentStream.restoreGraphicsState(); } catch (final IOException e) { } } } private void drawLine(final LineString line) throws IOException { for (int i = 0; i < line.getVertexCount(); i++) { final double modelX = line.getX(i); final double modelY = line.getY(i); final double[] viewCoordinates = toViewCoordinates(modelX, modelY); final float viewX = (float)viewCoordinates[0]; final float viewY = (float)(getViewHeightPixels() - viewCoordinates[1]); if (i == 0) { this.contentStream.moveTo(viewX, viewY); } else { this.contentStream.lineTo(viewX, viewY); } } } private void drawLineReverse(final LineString line) throws IOException { final int toVertexIndex = line.getVertexCount() - 1; for (int i = toVertexIndex; i >= 0; i--) { final double modelX = line.getX(i); final double modelY = line.getY(i); final double[] viewCoordinates = toViewCoordinates(modelX, modelY); final float viewX = (float)viewCoordinates[0]; final float viewY = (float)(getViewHeightPixels() - viewCoordinates[1]); if (i == toVertexIndex) { this.contentStream.moveTo(viewX, viewY); } else { this.contentStream.lineTo(viewX, viewY); } } } @Override public void drawText(final Record record, final Geometry geometry, final TextStyle style) { try { final String label = TextStyleRenderer.getLabel(record, style); if (Property.hasValue(label) && geometry != null) { final String textPlacementType = style.getTextPlacementType(); final PointDoubleXYOrientation point = AbstractRecordLayerRenderer .getPointWithOrientation(this, geometry, textPlacementType); if (point != null) { final double orientation = point.getOrientation(); this.contentStream.saveGraphicsState(); try { // style.setTextStyle(viewport, graphics); final double x = point.getX(); final double y = point.getY(); final double[] location = toViewCoordinates(x, y); // style.setTextStyle(viewport, graphics); final Measure<Length> textDx = style.getTextDx(); double dx = Viewport2D.toDisplayValue(this, textDx); final Measure<Length> textDy = style.getTextDy(); double dy = -Viewport2D.toDisplayValue(this, textDy); final Font font = style.getFont(this); final FontMetrics fontMetrics = this.canvas.getFontMetrics(font); double maxWidth = 0; final String[] lines = label.split("[\\r\\n]"); for (final String line : lines) { final Rectangle2D bounds = fontMetrics.getStringBounds(line, this.canvas.getGraphics()); final double width = bounds.getWidth(); maxWidth = Math.max(width, maxWidth); } final int descent = fontMetrics.getDescent(); final int ascent = fontMetrics.getAscent(); final int leading = fontMetrics.getLeading(); final double maxHeight = lines.length * (ascent + descent) + (lines.length - 1) * leading; final String verticalAlignment = style.getTextVerticalAlignment(); if ("top".equals(verticalAlignment)) { } else if ("middle".equals(verticalAlignment)) { dy -= maxHeight / 2; } else { dy -= maxHeight; } String horizontalAlignment = style.getTextHorizontalAlignment(); double screenX = location[0]; double screenY = getViewHeightPixels() - location[1]; final String textPlacement = style.getTextPlacementType(); if ("auto".equals(textPlacement)) { if (screenX < 0) { screenX = 1; dx = 0; horizontalAlignment = "left"; } final int viewWidth = getViewWidthPixels(); if (screenX + maxWidth > viewWidth) { screenX = (int)(viewWidth - maxWidth - 1); dx = 0; horizontalAlignment = "left"; } if (screenY < maxHeight) { screenY = 1; dy = 0; } final int viewHeight = getViewHeightPixels(); if (screenY > viewHeight) { screenY = viewHeight - 1 - maxHeight; dy = 0; } } AffineTransform transform = new AffineTransform(); transform.translate(screenX, screenY); if (orientation != 0) { transform.rotate(-Math.toRadians(orientation), 0, 0); } transform.translate(dx, dy); for (final String line : lines) { transform.translate(0, ascent); final AffineTransform lineTransform = new AffineTransform(transform); final Rectangle2D bounds = fontMetrics.getStringBounds(line, this.canvas.getGraphics()); final double width = bounds.getWidth(); final double height = bounds.getHeight(); if ("right".equals(horizontalAlignment)) { transform.translate(-width, 0); } else if ("center".equals(horizontalAlignment)) { transform.translate(-width / 2, 0); } transform.translate(dx, 0); transform.scale(1, 1); if (Math.abs(orientation) > 90) { transform.rotate(Math.PI, maxWidth / 2, -height / 4); } /* * final double textHaloRadius = Viewport2D.toDisplayValue(this, * style.getTextHaloRadius()); if (textHaloRadius > 0) { * graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, * RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB); final Stroke savedStroke = * graphics.getStroke(); final Stroke outlineStroke = new BasicStroke( * (float)textHaloRadius, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL); * graphics.setColor(style.getTextHaloFill()); graphics.setStroke(outlineStroke); * final Font font = graphics.getFont(); final FontRenderContext fontRenderContext = * graphics.getFontRenderContext(); final TextLayout textLayout = new TextLayout(line, * font, fontRenderContext); final Shape outlineShape = * textLayout.getOutline(TextStyleRenderer.NOOP_TRANSFORM); * graphics.draw(outlineShape); graphics.setStroke(savedStroke); } */ final Color textBoxColor = style.getTextBoxColor(); if (textBoxColor != null) { this.contentStream.setNonStrokingColor(textBoxColor); final double cornerSize = Math.max(height / 2, 5); // final RoundRectangle2D.Double box = new // RoundRectangle2D.Double( // bounds.getX() - 3, bounds.getY() - 1, width + 6, height + 2, // cornerSize, cornerSize); this.contentStream.fillRect((float)bounds.getX() - 3, (float)bounds.getY() - 1, (float)width + 6, (float)height + 2); } this.contentStream.setNonStrokingColor(style.getTextFill()); this.contentStream.beginText(); final PDFont pdfFont = getFont("/org/apache/pdfbox/resources/ttf/ArialMT.ttf"); this.contentStream.setFont(pdfFont, font.getSize2D()); this.contentStream.setTextMatrix(transform); this.contentStream.drawString(line); this.contentStream.endText(); transform = lineTransform; transform.translate(0, leading + descent); } } finally { this.contentStream.restoreGraphicsState(); } } } } catch (final IOException e) { throw new RuntimeException("Unable to write PDF", e); } } private PDFont getFont(final String path) throws IOException { PDFont font = this.fonts.get(path); if (font == null) { final InputStream fontStream = PDDocument.class .getResourceAsStream("/org/apache/pdfbox/resources/ttf/ArialMT.ttf"); font = PDTrueTypeFont.loadTTF(this.document, fontStream); this.fonts.put("/org/apache/pdfbox/resources/ttf/ArialMT.ttf", font); } return font; } @Override public boolean isHidden(final AbstractRecordLayer layer, final LayerRecord record) { return false; } private void setGeometryStyle(final GeometryStyle style) throws IOException { String styleName = this.styleNames.get(style); if (styleName == null) { styleName = "rgStyle" + this.styleId++; final PDExtendedGraphicsState graphicsState = new PDExtendedGraphicsState(); final int lineOpacity = style.getLineOpacity(); if (lineOpacity != 255) { graphicsState.setStrokingAlphaConstant(lineOpacity / 255f); } final Measure<Length> lineWidth = style.getLineWidth(); final Unit<Length> unit = lineWidth.getUnit(); graphicsState.setLineWidth((float)toDisplayValue(lineWidth)); final List<Double> lineDashArray = style.getLineDashArray(); if (lineDashArray != null && !lineDashArray.isEmpty()) { int size = lineDashArray.size(); if (size == 1) { size++; } final float[] dashArray = new float[size]; for (int i = 0; i < dashArray.length; i++) { if (i < lineDashArray.size()) { final Double dashDouble = lineDashArray.get(i); final Measure<Length> dashMeasure = Measure.valueOf(dashDouble, unit); final float dashFloat = (float)toDisplayValue(dashMeasure); dashArray[i] = dashFloat; } else { dashArray[i] = dashArray[i - 1]; } } final int offset = (int)toDisplayValue(Measure.valueOf(style.getLineDashOffset(), unit)); final COSArray dashCosArray = new COSArray(); dashCosArray.setFloatArray(dashArray); final PDLineDashPattern pattern = new PDLineDashPattern(dashCosArray, offset); graphicsState.setLineDashPattern(pattern); } switch (style.getLineCap()) { case BUTT: graphicsState.setLineCapStyle(0); break; case ROUND: graphicsState.setLineCapStyle(1); break; case SQUARE: graphicsState.setLineCapStyle(2); break; } switch (style.getLineJoin()) { case MITER: graphicsState.setLineJoinStyle(0); break; case ROUND: graphicsState.setLineJoinStyle(1); break; case BEVEL: graphicsState.setLineJoinStyle(2); break; } final int polygonFillOpacity = style.getPolygonFillOpacity(); if (polygonFillOpacity != 255) { graphicsState.setNonStrokingAlphaConstant(polygonFillOpacity / 255f); } final PDResources resources = this.page.findResources(); Map<String, PDExtendedGraphicsState> graphicsStateDictionary = resources.getGraphicsStates(); if (graphicsStateDictionary == null) { graphicsStateDictionary = new TreeMap<>(); } graphicsStateDictionary.put(styleName, graphicsState); resources.setGraphicsStates(graphicsStateDictionary); this.styleNames.put(style, styleName); } this.contentStream.appendRawCommands("/" + styleName + " gs\n"); } }