/** * Copyright (c) Codice Foundation * <p> * This is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser * General Public License as published by the Free Software Foundation, either version 3 of the * License, or any later version. * <p> * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. A copy of the GNU Lesser General Public License * is distributed along with this program and can be found at * <http://www.gnu.org/licenses/lgpl.html>. */ package ddf.catalog.transformer.input.pdf; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.apache.pdfbox.cos.COSArray; import org.apache.pdfbox.cos.COSBase; import org.apache.pdfbox.cos.COSBoolean; import org.apache.pdfbox.cos.COSDictionary; import org.apache.pdfbox.cos.COSDocument; import org.apache.pdfbox.cos.COSFloat; import org.apache.pdfbox.cos.COSInteger; import org.apache.pdfbox.cos.COSName; import org.apache.pdfbox.cos.COSNull; import org.apache.pdfbox.cos.COSStream; import org.apache.pdfbox.cos.COSString; import org.apache.pdfbox.cos.ICOSVisitor; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class GeoPdfParserImpl implements GeoPdfParser { public static final String GEOGRAPHIC = "GEOGRAPHIC"; public static final String LGIDICT = "LGIDict"; public static final String PROJECTION = "Projection"; public static final String PROJECTION_TYPE = "ProjectionType"; public static final String NEATLINE = "Neatline"; public static final String CTM = "CTM"; private static final String POLYGON = "POLYGON (("; private static final String MULTIPOLYGON = "MULTIPOLYGON ("; private static final Logger LOGGER = LoggerFactory.getLogger(GeoPdfParserImpl.class); private static final int CTM_SIZE = 6; /** * Generates a WKT compliant String from a PDF Document if it contains GeoPDF information. * Currently, only WGS84 Projections are supported (GEOGRAPHIC GeoPDF ProjectionType). * * @param pdfDocument - The PDF document * @return the WKT String * @throws IOException */ @Override public String apply(PDDocument pdfDocument) throws IOException { ToDoubleVisitor toDoubleVisitor = new ToDoubleVisitor(); LinkedList<String> polygons = new LinkedList<>(); for (PDPage pdPage : pdfDocument.getPages()) { COSDictionary cosObject = pdPage.getCOSObject(); COSBase lgiDictObject = cosObject.getObjectFromPath(LGIDICT); // Handle Multiple Map Frames if (lgiDictObject instanceof COSArray) { for (int i = 0; i < ((COSArray) lgiDictObject).size(); i++) { COSDictionary lgidict = (COSDictionary) cosObject.getObjectFromPath( LGIDICT + "/[" + i + "]"); COSDictionary projectionArray = (COSDictionary) lgidict.getDictionaryObject( PROJECTION); if (projectionArray != null) { String projectionType = ((COSString) projectionArray.getItem(PROJECTION_TYPE)).getString(); if (GEOGRAPHIC.equals(projectionType)) { COSArray neatlineArray = (COSArray) cosObject.getObjectFromPath( LGIDICT + "/[" + i + "]/" + NEATLINE); String wktString = getWktFromNeatLine(lgidict, neatlineArray, toDoubleVisitor); polygons.add(wktString); } else { LOGGER.debug( "Unsupported projection type {}. Map Frame will be skipped.", projectionType); } } else { LOGGER.debug( "No projection array found on the map frame. Map Frame will be skipped."); } } // Handle One Map Frame } else if (lgiDictObject instanceof COSDictionary) { COSDictionary lgidict = (COSDictionary) lgiDictObject; COSDictionary projectionArray = (COSDictionary) lgidict.getDictionaryObject( PROJECTION); if (projectionArray != null) { String projectionType = ((COSString) projectionArray.getItem(PROJECTION_TYPE)).getString(); if (GEOGRAPHIC.equals(projectionType)) { COSArray neatlineArray = (COSArray) cosObject.getObjectFromPath( LGIDICT + "/" + NEATLINE); if (neatlineArray == null) { neatlineArray = generateNeatLineFromPDFDimensions(pdPage); } polygons.add(getWktFromNeatLine(lgidict, neatlineArray, toDoubleVisitor)); } else { LOGGER.debug("Unsupported projection type {}. Map Frame will be skipped.", projectionType); } } else { LOGGER.debug( "No projection array found on the map frame. Map Frame will be skipped."); } } } if (polygons.size() == 0) { LOGGER.debug( "No GeoPDF information found on PDF during transformation. Metacard location will not be set."); return null; } if (polygons.size() == 1) { return POLYGON + polygons.get(0) + "))"; } else { return polygons.stream() .map(polygon -> "((" + polygon + "))") .collect(Collectors.joining(",", MULTIPOLYGON, ")")); } } /** * A PDF Neatline defines the area of a PDF image with relation to the PDF page. If no neatline is given * it is assumed that the image encompasses the entire page. This functiong generates a NeatLine * in this fashion. * @param pdPage the page to generate the NeatLine * @return an array of points representing the NeatLine */ private COSArray generateNeatLineFromPDFDimensions(PDPage pdPage) { COSArray neatLineArray = new COSArray(); String width = String.valueOf(pdPage.getMediaBox().getWidth()); String height = String.valueOf(pdPage.getMediaBox().getHeight()); neatLineArray.add(new COSString("0")); neatLineArray.add(new COSString("0")); neatLineArray.add(new COSString(width)); neatLineArray.add(new COSString("0")); neatLineArray.add(new COSString(width)); neatLineArray.add(new COSString(height)); neatLineArray.add(new COSString("0")); neatLineArray.add(new COSString(height)); return neatLineArray; } /** * Convert a Point2d into WKT Lat/Lon * * @param point2D * @return a String representation of a WKT Lat/Lon pair */ private String point2dToWkt(Point2D point2D) { return point2D.getX() + " " + point2D.getY(); } /** * Parses a given NeatLine and Transformation matrix into a WKT String * * @param lgidict - The PDF's LGIDict object * @param neatLineArray - The NeatLine array of points for the PDF * @param toDoubleVisitor - A visitor that converts PDF Strings / Ints / Longs into doubles. * @return the generated WKT Lat/Lon set * @throws IOException */ private String getWktFromNeatLine(COSDictionary lgidict, COSArray neatLineArray, ICOSVisitor toDoubleVisitor) throws IOException { List<Double> neatline = new LinkedList<>(); List<String> coordinateList = new LinkedList<>(); String firstCoordinate = null; double[] points = new double[CTM_SIZE]; for (int i = 0; i < CTM_SIZE; i++) { points[i] = (Double) lgidict.getObjectFromPath(CTM + "/[" + i + "]") .accept(toDoubleVisitor); } AffineTransform affineTransform = new AffineTransform(points); for (int i = 0; i < neatLineArray.size(); i++) { neatline.add((Double) neatLineArray.get(i) .accept(toDoubleVisitor)); } for (int i = 0; i < neatline.size(); i += 2) { double x = neatline.get(i); double y = neatline.get(i + 1); Point2D p = new Point2D.Double(x, y); Point2D pprime = affineTransform.transform(p, null); String xySet = point2dToWkt(pprime); if (firstCoordinate == null) { firstCoordinate = xySet; } coordinateList.add(xySet); } coordinateList.add(firstCoordinate); String wktString = StringUtils.join(coordinateList, ", "); LOGGER.debug("{}", wktString); return wktString.toString(); } /** * This visitor class converts parsable COS Objects into {@link Double}s */ private static class ToDoubleVisitor implements ICOSVisitor { @Override public Object visitFromArray(COSArray cosArray) throws IOException { return null; } @Override public Object visitFromBoolean(COSBoolean cosBoolean) throws IOException { return null; } @Override public Object visitFromDictionary(COSDictionary cosDictionary) throws IOException { return null; } @Override public Object visitFromDocument(COSDocument cosDocument) throws IOException { return null; } @Override public Object visitFromFloat(COSFloat cosFloat) throws IOException { return cosFloat.doubleValue(); } @Override public Object visitFromInt(COSInteger cosInteger) throws IOException { return (double) cosInteger.longValue(); } @Override public Object visitFromName(COSName cosName) throws IOException { return null; } @Override public Object visitFromNull(COSNull cosNull) throws IOException { return null; } @Override public Object visitFromStream(COSStream cosStream) throws IOException { return null; } @Override public Object visitFromString(COSString cosString) throws IOException { return Double.valueOf(cosString.getString()); } } }