// License: GPL. For details, see LICENSE file. package pdfimport; import java.awt.Color; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public class PathOptimizer { public List<Point2D> uniquePoints; public Map<Point2D, Point2D> uniquePointMap; private final Map<LayerInfo, LayerContents> layerMap; private List<LayerContents> layers; public Rectangle2D bounds; private final double pointsTolerance; private final Color color; boolean splitOnColorChange; LayerContents prevLayer = null; public PathOptimizer(double _pointsTolerance, Color _color, boolean _splitOnColorChange) { uniquePointMap = new HashMap<>(); uniquePoints = new ArrayList<>(); layerMap = new HashMap<>(); layers = new ArrayList<>(); pointsTolerance = _pointsTolerance; color = _color; splitOnColorChange = _splitOnColorChange; } public Point2D getUniquePoint(Point2D point) { if (uniquePointMap.containsKey(point)) { return uniquePointMap.get(point); } else { uniquePointMap.put(point, point); uniquePoints.add(point); return point; } } public void addPath(LayerInfo info, PdfPath path) { if (!isColorOK(info)) { return; } LayerContents layer = this.getLayer(info); layer.paths.add(path); } public void addMultiPath(LayerInfo info, List<PdfPath> paths) { if (!isColorOK(info)) { return; } LayerContents layer = this.getLayer(info); //optimize the paths Set<Point2D> points = new HashSet<>(); for (PdfPath path: paths) { points.addAll(path.points); } LayerContents multipathLayer = new LayerContents(); multipathLayer.paths = paths; Map<Point2D, Point2D> pointMap = DuplicateNodesFinder.findDuplicateNodes(points, pointsTolerance); this.fixPoints(multipathLayer, pointMap); this.concatenatePaths(multipathLayer); paths = multipathLayer.paths; boolean goodMultiPath = true; for (PdfPath path: paths) { goodMultiPath &= path.isClosed(); } if (goodMultiPath) { PdfMultiPath p = new PdfMultiPath(paths); layer.multiPaths.add(p); } else { layer.paths.addAll(paths); } } private boolean isColorOK(LayerInfo info) { if (color == null) { return true; } int rgb = color.getRGB() & 0xffffff; boolean good = false; if (info.fill != null && (info.fill.getRGB() & 0xffffff) == rgb) { good = true; } if (info.stroke != null && (info.stroke.getRGB() & 0xffffff) == rgb) { good = true; } return good; } public void removeParallelLines(double maxDistance) { for (LayerContents layer: this.layers) { this.removeParallelLines(layer, maxDistance); } } public void mergeNodes() { Map<Point2D, Point2D> pointMap = DuplicateNodesFinder.findDuplicateNodes(uniquePoints, pointsTolerance); for (LayerContents layer: this.layers) { this.fixPoints(layer, pointMap); } } public void mergeSegments() { for (LayerContents layer: this.layers) { this.concatenatePaths(layer); } } public void removeSmallObjects(double tolerance) { for (LayerContents layer: this.layers) { this.removeSmallObjects(layer, tolerance, Double.POSITIVE_INFINITY); } } public void removeLargeObjects(double tolerance) { for (LayerContents layer: this.layers) { this.removeSmallObjects(layer, 0.0, tolerance); } } public void splitLayersBySimilarShapes(double tolerance) { List<LayerContents> newLayers = new ArrayList<>(); for (LayerContents l: this.layers) { List<LayerContents> splitResult = splitBySimilarGroups(l); for (LayerContents ll: splitResult) { newLayers.add(ll); } } this.layers = newLayers; } public void splitLayersByPathKind(boolean closed, boolean single, boolean orthogonal) { List<LayerContents> newLayers = new ArrayList<>(); for (LayerContents l: this.layers) { List<LayerContents> splitResult = splitBySegmentKind(l, closed, single, orthogonal); for (LayerContents ll: splitResult) { newLayers.add(ll); } } this.layers = newLayers; } public void finish() { int nr = 0; for (LayerContents layer: this.layers) { layer.info.nr = nr; nr++; finalizeLayer(layer); } } private LayerContents getLayer(LayerInfo info) { LayerContents layer = null; if (this.layerMap.containsKey(info)) { layer = this.layerMap.get(info); if (layer != this.prevLayer && this.splitOnColorChange) { layer = null; } } if (layer == null) { layer = new LayerContents(); layer.info = info.copy(); layer.info.nr = this.layers.size(); this.layerMap.put(layer.info, layer); this.layers.add(layer); } this.prevLayer = layer; return layer; } private void finalizeLayer(LayerContents layer) { Set<Point2D> points = new HashSet<>(); layer.points = new ArrayList<>(); for (PdfPath pp: layer.paths) { pp.layer = layer; for (Point2D point: pp.points) { if (!points.contains(point)) { layer.points.add(point); points.add(point); } } } for (PdfMultiPath multipath: layer.multiPaths) { multipath.layer = layer; for (PdfPath pp: multipath.paths) { pp.layer = layer; for (Point2D point: pp.points) { if (!points.contains(point)) { layer.points.add(point); points.add(point); } } } } } private void fixPoints(LayerContents layer, Map<Point2D, Point2D> pointMap) { List<PdfPath> newPaths = new ArrayList<>(layer.paths.size()); for (PdfPath path: layer.paths) { List<Point2D> points = fixPoints(path.points, pointMap); path.points = points; if (points.size() > 2 || (!path.isClosed() && points.size() > 1)) { newPaths.add(path); } } layer.paths = newPaths; for (PdfMultiPath mp: layer.multiPaths) { for (PdfPath path: mp.paths) { path.points = fixPoints(path.points, pointMap); } } } private List<Point2D> fixPoints(List<Point2D> points, Map<Point2D, Point2D> pointMap) { List<Point2D> newPoints = new ArrayList<>(points.size()); Point2D prevPoint = null; for (Point2D p: points) { Point2D pp = p; if (pointMap.containsKey(p)) { pp = pointMap.get(p); } if (prevPoint != pp) { newPoints.add(pp); } prevPoint = pp; } return newPoints; } private void removeSmallObjects(LayerContents layer, double min, double max) { List<PdfPath> newPaths = new ArrayList<>(layer.paths.size()); for (PdfPath path: layer.paths) { double size = getShapeSize(path); boolean good = size >= min && size <= max; if (good) { newPaths.add(path); } } layer.paths = newPaths; List<PdfMultiPath> newMPaths = new ArrayList<>(layer.multiPaths.size()); for (PdfMultiPath mp: layer.multiPaths) { boolean good = true; for (PdfPath path: mp.paths) { double size = getShapeSize(path); good &= size >= min && size <= max; } if (good) { newMPaths.add(mp); } } layer.multiPaths = newMPaths; } private double getShapeSize(PdfPath path) { Rectangle2D bounds = new Rectangle2D.Double(); bounds.setRect(path.points.get(0).getX(), path.points.get(0).getY(), 0, 0); for (Point2D n: path.points) { bounds.add(n); } return Math.max(bounds.getWidth(), bounds.getHeight()); } /*** * This method finds parralel lines with similar distance and removes them. */ private void removeParallelLines(LayerContents layer, double maxDistance) { double angleTolerance = 1.0 / 180.0 * Math.PI; // 1 degree int minSegments = 10; //filter paths by direction List<ParallelSegmentsFinder> angles = new ArrayList<>(); for (PdfPath path: layer.paths) { if (path.points.size() != 2) { continue; } Point2D p1 = path.firstPoint(); Point2D p2 = path.lastPoint(); double angle = Math.atan2(p2.getX() - p1.getX(), p2.getY() - p1.getY()); //normalize between 0 and 180 degrees while (angle < 0) angle += Math.PI; while (angle > Math.PI) angle -= Math.PI; boolean added = false; for (ParallelSegmentsFinder pa: angles) { if (Math.abs(pa.angle - angle) < angleTolerance) { pa.addPath(path, angle); added = true; break; } } if (!added) { ParallelSegmentsFinder pa = new ParallelSegmentsFinder(); pa.addPath(path, angle); angles.add(pa); } } Set<PdfPath> pathsToRemove = new HashSet<>(); //process each direction for (ParallelSegmentsFinder pa: angles) { if (pa.paths.size() < minSegments) { continue; } List<ParallelSegmentsFinder> parts = pa.splitByDistance(maxDistance); for (ParallelSegmentsFinder part: parts) { if (part.paths.size() >= minSegments) { pathsToRemove.addAll(part.paths); } } } //generate new path list List<PdfPath> result = new ArrayList<>(layer.paths.size() - pathsToRemove.size()); for (PdfPath path: layer.paths) { if (!pathsToRemove.contains(path)) { result.add(path); } } layer.paths = result; } /** * This method merges together paths with common end nodes. * @param layer the layer to process. */ private void concatenatePaths(LayerContents layer) { Map<Point2D, List<PdfPath>> pathEndpoints = new HashMap<>(); Set<PdfPath> mergedPaths = new HashSet<>(); List<PdfPath> newPaths = new ArrayList<>(); //fill pathEndpoints map for (PdfPath pp: layer.paths) { if (pp.isClosed()) { newPaths.add(pp); continue; } List<PdfPath> paths = pathEndpoints.get(pp.firstPoint()); if (paths == null) { paths = new ArrayList<>(2); pathEndpoints.put(pp.firstPoint(), paths); } paths.add(pp); paths = pathEndpoints.get(pp.lastPoint()); if (paths == null) { paths = new ArrayList<>(2); pathEndpoints.put(pp.lastPoint(), paths); } paths.add(pp); } List<PdfPath> pathChain = new ArrayList<>(2); Set<Point2D> pointsInPath = new HashSet<>(); //join the paths for (PdfPath pp: layer.paths) { if (pp.isClosed() || mergedPaths.contains(pp)) { continue; } boolean changed = true; PdfPath firstPath = pp; PdfPath lastPath = pp; Point2D firstPoint = pp.firstPoint(); Point2D lastPoint = pp.lastPoint(); pathChain.clear(); pathChain.add(pp); pointsInPath.clear(); pointsInPath.add(firstPoint); pointsInPath.add(lastPoint); //process last point while (changed && firstPoint != lastPoint) { changed = false; List<PdfPath> adjacentPaths = pathEndpoints.get(lastPoint); PdfPath nextPath = findNextPath(adjacentPaths, lastPath); if (nextPath != null) { Point2D nextPoint = nextPath.getOtherEnd(lastPoint); lastPoint = nextPoint; lastPath = nextPath; pathChain.add(lastPath); if (!pointsInPath.contains(lastPoint)) { pointsInPath.add(lastPoint); changed = true; } else { //closed path found //remove excess segments from start of chain while (lastPoint != firstPoint) { PdfPath pathToRemove = pathChain.remove(0); firstPoint = pathToRemove.getOtherEnd(firstPoint); } changed = false; } } } //process first point changed = true; while (changed && firstPoint != lastPoint) { changed = false; List<PdfPath> adjacentPaths = pathEndpoints.get(firstPoint); PdfPath nextPath = findNextPath(adjacentPaths, firstPath); if (nextPath != null) { Point2D nextPoint = nextPath.getOtherEnd(firstPoint); firstPoint = nextPoint; firstPath = nextPath; pathChain.add(0, firstPath); if (!pointsInPath.contains(firstPoint)) { pointsInPath.add(firstPoint); changed = true; } else { //closed path found //remove excess segments from end of chain while (lastPoint != firstPoint) { PdfPath pathToRemove = pathChain.remove(pathChain.size() - 1); lastPoint = pathToRemove.getOtherEnd(lastPoint); } changed = false; } } } //remove from map for (PdfPath path: pathChain) { pathEndpoints.get(path.firstPoint()).remove(path); pathEndpoints.get(path.lastPoint()).remove(path); mergedPaths.add(path); } //construct path PdfPath path = pathChain.get(0); for (int pos = 1; pos < pathChain.size(); pos++) { path.points = tryMergeNodeLists(path.points, pathChain.get(pos).points); if (path.points == null) { throw new RuntimeException(); } } newPaths.add(path); } layer.paths = newPaths; } private PdfPath findNextPath(List<PdfPath> adjacentPaths, PdfPath firstPath) { for (int pos = 0; pos < adjacentPaths.size(); pos++) { PdfPath p = adjacentPaths.get(pos); if (p != firstPath && !isSubpathOf(firstPath, p)) { return p; } } return null; } /** * Tests if sub is subpath of main. */ private boolean isSubpathOf(PdfPath main, PdfPath sub) { Set<Point2D> points = new HashSet<>(main.points); for (Point2D point: sub.points) { if (!points.contains(point)) { return false; } } return true; } private List<LayerContents> splitBySegmentKind(LayerContents layer, boolean closed, boolean single, boolean orthogonal) { if (!closed && !single) { return Collections.singletonList(layer); } OrthogonalShapesFilter of = new OrthogonalShapesFilter(10); List<PdfPath> singleSegmentPaths = new ArrayList<>(); List<PdfPath> multiSegmentPaths = new ArrayList<>(); List<PdfPath> closedPaths = new ArrayList<>(); List<PdfPath> orthogonalPaths = new ArrayList<>(); List<PdfPath> orthogonalClosedPaths = new ArrayList<>(); for (PdfPath path: layer.paths) { boolean pathOrthgonal = orthogonal && of.isOrthogonal(path); boolean pathUnclosed = !path.isClosed() && closed; boolean pathSingleSegment = path.points.size() <= 3 && single; if (pathSingleSegment) { singleSegmentPaths.add(path); } else if (pathUnclosed) { if (pathOrthgonal) { orthogonalPaths.add(path); } else { multiSegmentPaths.add(path); } } else { if (pathOrthgonal) { orthogonalClosedPaths.add(path); } else { closedPaths.add(path); } } } List<LayerContents> layers = new ArrayList<>(); if (multiSegmentPaths.size() > 0) { LayerContents l = new LayerContents(); l.paths = multiSegmentPaths; l.info = layer.info.copy(); layers.add(l); } if (singleSegmentPaths.size() > 0) { LayerContents l = new LayerContents(); l.paths = singleSegmentPaths; l.info = layer.info.copy(); layers.add(l); } if (orthogonalPaths.size() > 0) { LayerContents l = new LayerContents(); l.paths = orthogonalPaths; l.info = layer.info.copy(); layers.add(l); } if (orthogonalClosedPaths.size() > 0) { LayerContents l = new LayerContents(); l.paths = orthogonalClosedPaths; l.info = layer.info.copy(); layers.add(l); } if (closedPaths.size() > 0 || layer.multiPaths.size() > 0) { LayerContents l = new LayerContents(); l.paths = closedPaths; l.info = layer.info.copy(); l.multiPaths = layer.multiPaths; layers.add(l); } return layers; } private List<LayerContents> splitBySimilarGroups(LayerContents layer) { List<List<PdfPath>> subparts = new ArrayList<>(); //split into similar parts for (PdfPath path: layer.paths) { List<PdfPath> sublayer = null; for (List<PdfPath> ll: subparts) { if (this.pathsSimilar(ll.get(0).points, path.points)) { sublayer = ll; break; } } if (sublayer == null) { sublayer = new ArrayList<>(); subparts.add(sublayer); } sublayer.add(path); } //get groups int minGroupTreshold = 10; List<PdfPath> independantPaths = new ArrayList<>(); List<LayerContents> result = new ArrayList<>(); for (List<PdfPath> list: subparts) { if (list.size() >= minGroupTreshold) { LayerContents l = new LayerContents(); l.paths = list; l.info = layer.info.copy(); l.info.isGroup = true; l.multiPaths = Collections.emptyList(); result.add(l); } else { independantPaths.addAll(list); } } if (independantPaths.size() > 0 || layer.multiPaths.size() > 0) { LayerContents l = new LayerContents(); l.paths = independantPaths; l.info = layer.info.copy(); l.info.isGroup = false; l.multiPaths = layer.multiPaths; result.add(l); } return result; } private List<Point2D> tryMergeNodeLists(List<Point2D> nodes1, List<Point2D> nodes2) { boolean nodes1Closed = (nodes1.get(0) == nodes1.get(nodes1.size() - 1)); boolean nodes2Closed = (nodes2.get(0) == nodes2.get(nodes2.size() - 1)); if (nodes1Closed || nodes2Closed) { return null; } if (nodes1.get(nodes1.size() - 1) == nodes2.get(0)) { nodes1.remove(nodes1.size() -1); nodes1.addAll(nodes2); return nodes1; } else if (nodes1.get(nodes1.size() - 1) == nodes2.get(nodes2.size() -1)) { nodes1.remove(nodes1.size() -1); for (int pos = nodes2.size() - 1; pos >= 0; pos--) { nodes1.add(nodes2.get(pos)); } return nodes1; } else if (nodes1.get(0) == nodes2.get(nodes2.size() - 1)) { nodes1.remove(0); nodes1.addAll(0, nodes2); return nodes1; } else if (nodes1.get(0) == nodes2.get(0)) { nodes1.remove(0); for (int pos = 0; pos < nodes2.size(); pos++) { nodes1.add(0, nodes2.get(pos)); } return nodes1; } else { return null; } } public List<LayerContents> getLayers() { return this.layers; } /** * Test if paths are different only by offset. */ private boolean pathsSimilar(List<Point2D> path1, List<Point2D> path2) { if (path1.size() != path2.size()) { return false; } if (path1.size() < 3) { return false; //cannot judge so small paths } Point2D p1 = path1.get(0); Point2D p2 = path2.get(0); double offsetX = p1.getX() - p2.getX(); double offsetY = p1.getY() - p2.getY(); double tolerance = 1e-4; for (int pos = 0; pos < path1.size(); pos++) { p1 = path1.get(pos); p2 = path2.get(pos); double errorX = p1.getX() - p2.getX() - offsetX; double errorY = p1.getY() - p2.getY() - offsetY; if (Math.abs(errorX) + Math.abs(errorY) > tolerance) { return false; } } return true; } }