/* -*- tab-width: 4 -*- * * Electric(tm) VLSI Design System * * File: Gerber.java * Input/output tool: Gerber input * Written by Steven M. Rubin, Sun Microsystems. * * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. * * Electric(tm) is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * Electric(tm) 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Electric(tm); see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place, Suite 330, * Boston, Mass 02111-1307, USA. */ package com.sun.electric.tool.io.input; import com.sun.electric.database.geometry.EPoint; import com.sun.electric.database.geometry.Poly; import com.sun.electric.database.geometry.PolyBase; import com.sun.electric.database.geometry.PolyMerge; import com.sun.electric.database.hierarchy.Cell; import com.sun.electric.database.hierarchy.Library; import com.sun.electric.database.id.CellId; import com.sun.electric.database.topology.ArcInst; import com.sun.electric.database.topology.Geometric; import com.sun.electric.database.topology.NodeInst; import com.sun.electric.database.topology.RTBounds; import com.sun.electric.technology.PrimitiveNode; import com.sun.electric.technology.Technology; import com.sun.electric.technology.technologies.Artwork; import com.sun.electric.tool.Job; import com.sun.electric.tool.io.IOTool; import com.sun.electric.util.TextUtils; import com.sun.electric.util.math.GenMath; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.File; import java.io.IOException; import java.io.LineNumberReader; import java.net.URL; import java.util.ArrayList; import java.util.BitSet; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; /** * This class reads files in Gerber files. */ public class Gerber extends Input<Object> { private static double UNSCALE = 1000.0; private static class StringPart { char separator; double value; } private static class StandardCircle { int codeNumber; double diameter; double holeSize1; double holeSize2; } private static class StandardRectangle { int codeNumber; double width, height; double holeWidth, holeHeight; } private static class GerberLayer { String layerName; boolean mentionedUse; boolean polarityDark; PrimitiveNode pNp; private static int ePrimitiveIndex = 0; private static PrimitiveNode [] ePrimitives = null; private static Map<String,GerberLayer> allLayers = new HashMap<String,GerberLayer>(); public GerberLayer(PrimitiveNode pNp) { this.pNp = pNp; polarityDark = true; mentionedUse = false; } public void usingIt() { if (mentionedUse) return; mentionedUse = true; System.out.println("Importing layer name '" + layerName + "' (polarity " + (polarityDark ? "dark" : "light") + ") into layer " + pNp.getName()); } public static GerberLayer findLayer(String name) { GerberLayer gl = allLayers.get(name); if (gl == null) { if (ePrimitives == null) { ePrimitives = new PrimitiveNode[16]; int j = 0; for(int i=0; i<8; i++) { ePrimitives[j++] = pcbTech.findNodeProto("Signal-" + (i+1) + "-Node"); ePrimitives[j++] = pcbTech.findNodeProto("Power-" + (i+1) + "-Node"); } } gl = new GerberLayer(ePrimitives[ePrimitiveIndex%ePrimitives.length]); ePrimitiveIndex++; allLayers.put(name, gl); gl.layerName = name; } return gl; } public void setPolarity(boolean dark) { polarityDark = dark; } } enum NumberFormat {OMIT_LEADING_ZEROS, OMIT_TRAILING_ZEROS, EXPLICIT_DECIMAL_POINT}; private GerberPreferences localPrefs; private Cell curCell; private Map<Integer,StandardCircle> standardCircles = new HashMap<Integer,StandardCircle>(); private Map<Integer,StandardRectangle> standardRectangles = new HashMap<Integer,StandardRectangle>(); private NumberFormat currentNumberFormat = NumberFormat.EXPLICIT_DECIMAL_POINT; private boolean absoluteCoordinates; private int xFormatLeft, xFormatRight, yFormatLeft, yFormatRight; private double scaleFactor; private boolean fullCircle = false; private int currentPrepCode, currentDCode; private double lastXValue=0, lastYValue=0, curXValue, curYValue, curIValue, curJValue; private String lastPart; private static GerberLayer defaultGerberLayer = new GerberLayer(Artwork.tech().filledPolygonNode); private GerberLayer currentLayer; List<Point2D> polygonPoints = null; private static Technology pcbTech = Technology.findTechnology("pcb"); public static class GerberPreferences extends InputPreferences { private boolean justThisFile; private boolean fillPolygons; public GerberPreferences(boolean factory) { super(factory); if (!factory) { justThisFile = !IOTool.isGerberReadsAllFiles(); fillPolygons = IOTool.isGerberFillsPolygons(); } } @Override public Library doInput(URL fileURL, Library lib, Technology tech, Map<Library,Cell> currentCells, Map<CellId,BitSet> nodesToExpand, Job job) { Gerber in = new Gerber(this); if (justThisFile) { if (in.openTextInput(fileURL)) return null; lib = in.importALibrary(lib, tech, currentCells); in.closeInput(); } else { lib = in.importALibrary(lib, tech, currentCells); } return lib; } } /** * Creates a new instance of Gerber. */ Gerber(GerberPreferences ap) { localPrefs = ap; } /** * Method to import a library from disk. * @param lib the library to fill * @param currentCells this map will be filled with currentCells in Libraries found in library file * @return the created library (null on error). */ protected Library importALibrary(Library lib, Technology tech, Map<Library,Cell> currentCells) { // make the cell String cellName = lib.getName(); curCell = Cell.makeInstance(lib, cellName + "{lay}"); if (localPrefs.justThisFile) { try { readFile(lineReader); } catch (IOException e) { System.out.println("ERROR reading Gerber file: " + e.getMessage()); } } else { // initialize the number of directories that need to be searched List<String> gerberfiles = new ArrayList<String>(); // determine the current directory String topDirName = TextUtils.getFilePath(lib.getLibFile()); // gerberfiles.add(topDirName); // find all files that end with ".gbr" and include them in the search File topDir = new File(topDirName); String [] fileList = topDir.list(); for(int i=0; i<fileList.length; i++) { if (!fileList[i].endsWith(".gbr")) continue; String fileName = topDirName + fileList[i]; gerberfiles.add(fileName); } // read the file try { for(String fileName : gerberfiles) { System.out.println("Reading: " + fileName); URL fileURL = TextUtils.makeURLToFile(fileName); if (openTextInput(fileURL)) return null; readFile(lineReader); closeInput(); } } catch (IOException e) { System.out.println("ERROR reading Gerber files: " + e.getMessage()); } } return lib; } /** * Method to read the Gerber file. */ private void readFile(LineNumberReader lr) throws IOException { currentLayer = defaultGerberLayer; lastPart = null; for(;;) { // get the next line of text String line = readSegment(lr); if (line == null) break; // handle RS274X "%" codes if (line.startsWith("%FS")) { handleFormatStatement(line); continue; } if (line.startsWith("%MO")) { handleEmbeddedUnits(line); continue; } if (line.startsWith("%IP")) { handleImagePolarity(line); continue; } if (line.startsWith("%AD")) { handleStandardShape(line); continue; } if (line.startsWith("%AS")) { // unknown! continue; } if (line.startsWith("%SF")) { // unknown! continue; } if (line.startsWith("%IN")) { // unknown! continue; } if (line.startsWith("%LN")) { // layer name int astPos = line.indexOf('*'); if (astPos < 0) astPos = line.length(); String layerName = line.substring(3, astPos); currentLayer = GerberLayer.findLayer(layerName); // if (currentLayer != null) // System.out.println("Importing layer name '" + layerName + "' into layer " + currentLayer.pNp.getName()); continue; } if (line.startsWith("%LP")) { // layer polarity if (line.charAt(3) == 'D') currentLayer.setPolarity(true); else currentLayer.setPolarity(false); continue; } handleOldStyleLine(line, lr); } } private String readSegment(LineNumberReader lr) throws IOException { String line; for(;;) { if (lastPart != null) { line = lastPart; lastPart = null; } else { line = lr.readLine(); if (line == null) return null; } if (line.length() > 0) break; } if (line.charAt(0) != '%') { // see if line breaks at "*" int astPos = line.indexOf('*'); if (astPos >= 0 && astPos+1 < line.length()) { lastPart = line.substring(astPos+1); line = line.substring(0, astPos+1); } } return line; } private void handleOldStyleLine(String line, LineNumberReader lr) { boolean foundCoord = false; List<StringPart> parts = parseString(line); for(StringPart sp : parts) { if (sp.separator == 'G') { int prepCode = (int)sp.value; switch (prepCode) { case 04: // Comment case 57: // Comment return; case 36: // start polygon if (localPrefs.fillPolygons) polygonPoints = new ArrayList<Point2D>(); break; case 37: // end polygon if (polygonPoints != null) { double cX = 0, cY = 0; double minX = polygonPoints.get(0).getX(); double minY = polygonPoints.get(0).getY(); double maxX = minX, maxY = minY; for(Point2D pt : polygonPoints) { cX += pt.getX(); cY += pt.getY(); if (pt.getX() < minX) minX = pt.getX(); if (pt.getX() > maxX) maxX = pt.getX(); if (pt.getY() < minY) minY = pt.getY(); if (pt.getY() > maxY) maxY = pt.getY(); } cX /= polygonPoints.size(); cY /= polygonPoints.size(); Point2D [] points = new Point2D[polygonPoints.size()]; for(int i=0; i<polygonPoints.size(); i++) points[i] = polygonPoints.get(i); if (!currentLayer.polarityDark) { // see if this layer can be subtracted from another boolean subtracted = false; Poly subtractPoly = new Poly(points); Rectangle2D bounds = new Rectangle2D.Double(minX, minY, maxX-minX, maxY-minY); for(Iterator<RTBounds> it = curCell.searchIterator(bounds); it.hasNext(); ) { Geometric geom = (Geometric)it.next(); if (geom instanceof ArcInst) continue; NodeInst ni = (NodeInst)geom; if (ni.getProto().getTechnology() == pcbTech) { EPoint[] pts = ni.getTrace(); Point2D[] adjPts = new Point2D[pts.length]; for(int i=0; i<pts.length; i++) adjPts[i] = new Point2D.Double(pts[i].getX()+ni.getAnchorCenterX(), pts[i].getY()+ni.getAnchorCenterY()); Poly existing = new Poly(adjPts); //System.out.print("COMPARING FOUND POLYGON:"); //for(int i=0; i<points.length; i++) System.out.print(" ("+TextUtils.formatDouble(points[i].getX())+","+TextUtils.formatDouble(points[i].getY())+")"); //System.out.println(); //System.out.print("WITH SUBTRACT POLYGON:"); //for(int i=0; i<adjPts.length; i++) System.out.print(" ("+TextUtils.formatDouble(adjPts[i].getX())+","+TextUtils.formatDouble(adjPts[i].getY())+")"); //System.out.println(); PolyMerge merge = new PolyMerge(); merge.add(Artwork.tech().defaultLayer, existing); merge.subtract(Artwork.tech().defaultLayer, subtractPoly); List<PolyBase> polys = merge.getMergedPoints(Artwork.tech().defaultLayer, true); //if (polys.size() > 1) System.out.println("SUBTRACTION MADE "+polys.size()+" POLYGONS"); for(PolyBase pb : polys) { Point2D [] newPts = pb.getPoints(); ni.setTrace(newPts); subtracted = true; break; } } } if (subtracted) break; } PrimitiveNode pNp = currentLayer.pNp; currentLayer.usingIt(); Point2D ctr = new Point2D.Double(cX, cY); double width = maxX - minX; double height = maxY - minY; NodeInst ni = NodeInst.makeInstance(pNp, ctr, width, height, curCell); ni.setTrace(points); polygonPoints = null; } break; case 70: // inches scaleFactor = 25400000.0/UNSCALE; // 1 inch = 25,400,000 nm break; case 71: // millimeters scaleFactor = 1000000.0/UNSCALE; // 1 mm = 1,000,000 nm break; case 75: // full circles fullCircle = true; break; case 90: // Absolute coordinates absoluteCoordinates = true; break; case 91: // Relative coordinates absoluteCoordinates = false; break; case 1: case 2: case 3: case 10: case 11: case 12: case 60: currentPrepCode = prepCode; break; } continue; } if (sp.separator == 'D') { int dCode = (int)sp.value; if (dCode == 1 || dCode == 2) currentDCode = dCode; continue; } if (sp.separator == 'X') { curXValue = sp.value; foundCoord = true; continue; } if (sp.separator == 'Y') { curYValue = sp.value; foundCoord = true; continue; } if (sp.separator == 'I') { curIValue = sp.value; foundCoord = true; continue; } if (sp.separator == 'J') { curJValue = sp.value; foundCoord = true; continue; } } if (!foundCoord) return; // handle lines if (currentPrepCode == 12 || currentPrepCode == 11 || currentPrepCode == 01 || currentPrepCode == 10 || currentPrepCode == 60) { switch (currentPrepCode) { case 12: curXValue /= 100; curYValue /= 100; break; case 11: curXValue /= 10; curYValue /= 10; break; case 10: curXValue *= 10; curYValue *= 10; break; case 60: curXValue *= 100; curYValue *= 100; break; } if (!absoluteCoordinates) { curXValue += lastXValue; curYValue += lastYValue; } if (currentDCode == 1) { Point2D ctr = new Point2D.Double((curXValue+lastXValue)/2*scaleFactor, (curYValue+lastYValue)/2*scaleFactor); double width = Math.abs(curXValue - lastXValue)*scaleFactor; double height = Math.abs(curYValue - lastYValue)*scaleFactor; Point2D pt1 = new Point2D.Double(lastXValue*scaleFactor, lastYValue*scaleFactor); Point2D pt2 = new Point2D.Double(curXValue*scaleFactor, curYValue*scaleFactor); Point2D [] points = new Point2D[] {pt1, pt2}; if (polygonPoints != null) { addPolygonPoints(points); } else { PrimitiveNode pNp = currentLayer.pNp; currentLayer.usingIt(); NodeInst ni = NodeInst.makeInstance(pNp, ctr, width, height, curCell); if (width != 0 && height != 0) ni.setTrace(points); } } else if (currentDCode == 3) { PrimitiveNode pNp = Artwork.tech().filledCircleNode; Point2D ctr = new Point2D.Double(curXValue*scaleFactor, curYValue*scaleFactor); double width = 1; double height = 1; NodeInst.makeInstance(pNp, ctr, width, height, curCell); } lastXValue = curXValue; lastYValue = curYValue; } // handle circles if (currentPrepCode == 2 || currentPrepCode == 3) { if (!absoluteCoordinates) { curXValue += lastXValue; curYValue += lastYValue; } if (currentDCode == 1) { Point2D ctr = new Point2D.Double((lastXValue+curIValue)*scaleFactor, (lastYValue+curJValue)*scaleFactor); Point2D curveStart = new Point2D.Double(lastXValue*scaleFactor, lastYValue*scaleFactor); Point2D curveEnd = new Point2D.Double(curXValue*scaleFactor, curYValue*scaleFactor); if (currentPrepCode == 3) { Point2D swap = curveStart; curveStart = curveEnd; curveEnd = swap; } double diameter = ctr.distance(curveStart) * 2; double rotation = GenMath.figureAngle(ctr, curveEnd)*Math.PI/1800; double angle = GenMath.figureAngle(ctr, curveStart)*Math.PI/1800 - rotation; if (angle < 0) angle += Math.PI * 2; Point2D [] pointList = Artwork.fillEllipse(ctr, diameter, diameter, rotation, angle); if (polygonPoints != null) { addPolygonPoints(pointList); } else { PrimitiveNode pNp = currentLayer.pNp; currentLayer.usingIt(); NodeInst ni = NodeInst.makeInstance(pNp, ctr, diameter, diameter, curCell); Point2D[] doubledPointList = new Point2D[pointList.length*2-1]; for(int i=0; i<pointList.length; i++) { doubledPointList[i] = pointList[i]; if (i < pointList.length-1) doubledPointList[i+pointList.length] = pointList[pointList.length-i-2]; } ni.setTrace(doubledPointList); // PrimitiveNode pNp = Artwork.tech().circleNode; // NodeInst ni = NodeInst.makeInstance(pNp, ctr, diameter, diameter, curCell); // ni.setArcDegrees(rotation, angle); } } lastXValue = curXValue; lastYValue = curYValue; } } private void addPolygonPoints(Point2D[] pts) { if (polygonPoints.size() != 0) { Point2D first = polygonPoints.get(0); Point2D last = polygonPoints.get(polygonPoints.size()-1); Point2D firstNew = pts[0]; Point2D lastNew = pts[pts.length-1]; double dist1 = first.distance(firstNew); double dist2 = first.distance(lastNew); double dist3 = last.distance(firstNew); double dist4 = last.distance(lastNew); double minDist = Math.min(Math.min(dist1, dist2), Math.min(dist3, dist4)); if (dist1 == minDist) { for(int i=1; i<pts.length; i++) polygonPoints.add(0, pts[i]); return; } if (dist2 == minDist) { for(int i=pts.length-2; i>=0; i--) polygonPoints.add(0, pts[i]); return; } if (dist3 == minDist) { for(int i=1; i<pts.length; i++) polygonPoints.add(pts[i]); return; } if (dist4 == minDist) { for(int i=pts.length-2; i>=0; i--) polygonPoints.add(pts[i]); return; } } for(int i=0; i<pts.length; i++) polygonPoints.add(pts[i]); } /** * Handle the %FS format statement. * It has this syntax: * %FS{L|T|D}{A|I}(Nn)(Gn)(Xa)(Ya)(Zc)(Dn)(Mn)*% * where: * L = leading zeros omitted * T = trailing zeros omitted * D = explicit decimal point (i.e. no zeros omitted) * A = absolute coordinate mode * I = incremental coordinate mode * Nn = sequence number, where n is number of digits (rarely used) * Gn = prepartory function code (rarely used) * Xa = format of input data (5.5 is max) * Yb = format of input data * Zb = format of input data (Z is rarely if ever seen) * Dn = draft code * Mn = misc code * * @param line the format statement line. */ private void handleFormatStatement(String line) { switch (line.charAt(3)) { case 'L': currentNumberFormat = NumberFormat.OMIT_LEADING_ZEROS; break; case 'T': currentNumberFormat = NumberFormat.OMIT_TRAILING_ZEROS; break; case 'D': currentNumberFormat = NumberFormat.EXPLICIT_DECIMAL_POINT; break; } switch (line.charAt(4)) { case 'A': absoluteCoordinates = true; break; case 'I': absoluteCoordinates = false; break; } int pos = 5; for(;;) { if (line.charAt(pos) == '*') break; int value = TextUtils.atoi(line.substring(pos+1)); switch (line.charAt(pos)) { case 'N': break; case 'G': break; case 'X': xFormatLeft = value/10; xFormatRight = value%10; break; case 'Y': yFormatLeft = value/10; yFormatRight = value%10; break; case 'Z': break; case 'D': break; case 'M': break; } pos++; while (Character.isDigit(line.charAt(pos))) pos++; } } /** * Handle the %MO embedded units line. * It has this syntax: * %MOIN*% for inches * %MOMM*% for millimeters * @param line the embedded units line. */ private void handleEmbeddedUnits(String line) { if (line.equals("%MOIN*%")) { scaleFactor = 25400000.0/UNSCALE; // 1 inch = 25,400,000 nm } if (line.equals("%MOMM*%")) { scaleFactor = 1000000.0/UNSCALE; // 1 mm = 1,000,000 nm } } /** * Handle the %IP image polarity line. * It has this syntax: * %IPPOS*% for positive * %IPNEG*% for negative * @param line the image polarity line. */ private void handleImagePolarity(String line) { } /** * Handle the %AD standard circle/rectangle line. * It has this syntax: * %ADD{code}C,{$1}X{$2}X{$3}*% * where * AD - aperture description parameter * D{code} d-code to which this aperture is assigned (10-999) * C tells 274X this is a circle macro * $1 value (inches or mm) of the outside diameter * $2 optional, if present defines the diameter of the hole * $3 optional, if present the $2 and $3 represent the size of * a rectangular hole. * OR: * %ADD{code}R,{$1}X{$2}X{$3}X{$4}*% * where * AD - aperture description parameter * D{code} d-code to which this aperture is assigned (10-999) * R tells 274X this is a rectangle macro * $1 value (inches or mm) of rect's length in X * $2 value if rect's height in Y * $3 optional, if present defines the diameter of the hole * $4 optional, if present the $2 and $3 represent the size of * a rectangular hole. * @param line the standard circle line. */ private void handleStandardShape(String line) { int codeNumber = TextUtils.atoi(line.substring(4)); int pos = 4; while (Character.isDigit(line.charAt(pos))) pos++; if (line.charAt(pos) == 'C') { List<StringPart> parts = parseString(line.substring(pos+1)); if (parts.size() < 1 || parts.size() > 3) { System.out.println("Illegal Standard Circle statement: " + line); return; } StandardCircle sc = new StandardCircle(); sc.codeNumber = codeNumber; sc.diameter = parts.get(0).value; if (parts.size() > 1) sc.holeSize1 = parts.get(1).value; else sc.holeSize1 = -1; if (parts.size() > 2) sc.holeSize2 = parts.get(2).value; else sc.holeSize2 = -1; standardCircles.put(new Integer(codeNumber), sc); } else if (line.charAt(pos) == 'R') { List<StringPart> parts = parseString(line.substring(pos+1)); if (parts.size() != 2 || parts.size() != 4) { System.out.println("Illegal Standard Rectangle statement: " + line); return; } StandardRectangle sr = new StandardRectangle(); sr.codeNumber = codeNumber; sr.width = parts.get(0).value; sr.height = parts.get(1).value; if (parts.size() > 2) { sr.holeWidth = parts.get(2).value; sr.holeHeight = parts.get(3).value; } else { sr.holeWidth = -1; sr.holeHeight = -1; } standardRectangles.put(new Integer(codeNumber), sr); } } private List<StringPart> parseString(String str) { List<StringPart> parts = new ArrayList<StringPart>(); int pos = 0; StringBuffer sb = new StringBuffer(); for(;;) { if (str.charAt(pos) == '*') break; StringPart sp = new StringPart(); sp.separator = str.charAt(pos); pos++; sb.delete(0, sb.length()); boolean foundDecimal = false, neg = false; for(;;) { char nxt = str.charAt(pos++); if (nxt != '-' && nxt != '.' && !Character.isDigit(nxt)) break; if (nxt == '-') { neg = true; continue; } sb.append(nxt); if (nxt == '.') foundDecimal = true; } pos--; if (foundDecimal || sp.separator == 'G' || sp.separator == 'D' || sp.separator == 'M') { sp.value = TextUtils.atof(sb.toString()); } else { int left = xFormatLeft, right = xFormatRight; if (sp.separator == 'Y') { left = yFormatLeft; right = yFormatRight; } switch (currentNumberFormat) { case OMIT_LEADING_ZEROS: sb.insert(sb.length()-right, '.'); break; case OMIT_TRAILING_ZEROS: while (sb.length() < left) sb.append('0'); sb.insert(left, '.'); break; case EXPLICIT_DECIMAL_POINT: break; } sp.value = TextUtils.atof(sb.toString()); } if (neg) sp.value = -sp.value; parts.add(sp); } return parts; } }