/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3 as published by
the Free Software Foundation.
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 General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.cirqwizard.gerber;
import org.cirqwizard.gerber.appertures.*;
import org.cirqwizard.gerber.appertures.macro.*;
import org.cirqwizard.geom.Point;
import org.cirqwizard.logging.LoggerFactory;
import org.cirqwizard.settings.ApplicationConstants;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GerberParser
{
private ArrayList<GerberPrimitive> elements = new ArrayList<>();
private boolean parameterMode = false;
private ApertureMacro apertureMacro = null;
private HashMap<String, ApertureMacro> apertureMacros = new HashMap<>();
private Region region = null;
private HashMap<Integer, Aperture> apertures = new HashMap<>();
private static final int MM_RATIO = 1 * ApplicationConstants.RESOLUTION;
private static final int INCHES_RATIO = (int)(25.4 * ApplicationConstants.RESOLUTION);
private int unitConversionRatio = MM_RATIO;
private boolean omitLeadingZeros = true;
private int integerPlaces = 2;
private int decimalPlaces = 4;
private Reader reader;
private enum InterpolationMode
{
LINEAR,
CLOCKWISE_CIRCULAR,
COUNTERCLOCKWISE_CIRCULAR
}
private enum ArcQuadrantMode
{
SINGLE_QUADRANT, MULTI_QUADRANT
}
private InterpolationMode currentInterpolationMode = InterpolationMode.LINEAR;
private ArcQuadrantMode arcQuadrantMode;
private GerberPrimitive.Polarity polarity = GerberPrimitive.Polarity.DARK;
private int x = 0;
private int y = 0;
private enum ExposureMode
{
ON,
OFF,
FLASH
}
private ExposureMode exposureMode = ExposureMode.OFF;
private Aperture aperture = null;
public GerberParser(Reader reader)
{
this.reader = reader;
}
public List<GerberPrimitive> parse() throws IOException
{
String str;
while ((str = readDataBlock()) != null)
{
try
{
if (parameterMode)
parseParameter(str);
else
processDataBlock(parseDataBlock(str));
}
catch (GerberParsingException e)
{
LoggerFactory.getApplicationLogger().log(Level.FINE, "Unparsable gerber element", e);
}
}
return elements;
}
private String readDataBlock() throws IOException
{
StringBuilder sb = new StringBuilder();
int i;
while ((i = reader.read()) != -1)
{
if (i == '%')
{
parameterMode = !parameterMode;
apertureMacro = null;
}
else if (i == '*')
{
if (sb.length() > 0)
break;
}
else if (!Character.isWhitespace(i))
sb.append((char)i);
}
if (sb.length() == 0)
return null;
return sb.toString();
}
private void parseParameter(String parameter) throws GerberParsingException
{
if (apertureMacro != null)
parseApertureMacroDefinition(parameter);
if (parameter.startsWith("AD"))
parseApertureDefinition(parameter.substring(2));
else if (parameter.startsWith("OF") || parameter.startsWith("IP"))
LoggerFactory.getApplicationLogger().log(Level.FINE, "Ignoring obsolete gerber parameter");
else if (parameter.startsWith("FS"))
parseCoordinateFormatSpecification(parameter);
else if (parameter.startsWith("MO"))
parseMeasurementUnits(parameter.substring(2, parameter.length()));
else if (parameter.startsWith("AM"))
parseApertureMacro(parameter);
else if (parameter.startsWith("LP"))
parseLevelPolarity(parameter);
else
throw new GerberParsingException("Unknown parameter: " + parameter);
}
private void parseMeasurementUnits(String str)
{
if (str.equals("IN"))
unitConversionRatio = INCHES_RATIO;
else if (str.equals("MM"))
unitConversionRatio = MM_RATIO;
}
private void parseCoordinateFormatSpecification(String str)
{
omitLeadingZeros = str.charAt(2) == 'L';
integerPlaces = str.charAt(str.indexOf('X') + 1) - '0';
decimalPlaces = str.charAt(str.indexOf('X') + 2) - '0';
}
private void parseLevelPolarity(String str) throws GerberParsingException
{
if (str.charAt(2) == 'C')
polarity = GerberPrimitive.Polarity.CLEAR;
else if (str.charAt(2) == 'D')
polarity = GerberPrimitive.Polarity.DARK;
else
throw new GerberParsingException("Unknown polarity specified: " + str);
}
private void parseApertureMacro(String str)
{
String macroName = str.substring(2);
apertureMacro = new ApertureMacro();
apertureMacros.put(macroName, apertureMacro);
}
private final static Pattern PATTERN_MACRO_1 = Pattern.compile("1,(1|0),(\\d+.\\d+),(-?\\d+.\\d+),(-?\\d+.?\\d*)");
private final static Pattern PATTERN_MACRO_4 = Pattern.compile("4,(1|0),(\\d+),(.*),(-?\\d+.?\\d*)");
private final static Pattern PATTERN_MACRO_4_COORDINATE_PAIR = Pattern.compile("(-?\\d+.?\\d*),(-?\\d+.?\\d*)");
private final static Pattern PATTERN_MACRO_20 = Pattern.compile("20,(1|0),(\\d+.\\d+),(-?\\d+.\\d+),(-?\\d+.?\\d*),(-?\\d+.?\\d*),(-?\\d+.?\\d*),(-?\\d+.?\\d*)");
private final static Pattern PATTERN_MACRO_21 = Pattern.compile("21,(1|0),(\\d+.\\d+),(\\d+.\\d+),(-?\\d+.?\\d*),(-?\\d+.?\\d*),(-?\\d+.?\\d*)");
private void parseApertureMacroDefinition(String str)
{
Matcher matcher = PATTERN_MACRO_21.matcher(str);
if (matcher.find())
{
MacroCenterLine centerLine = new MacroCenterLine((int) (Double.valueOf(matcher.group(2)) * unitConversionRatio),
(int) (Double.valueOf(matcher.group(3)) * unitConversionRatio),
new Point((int) (Double.valueOf(matcher.group(4)) * unitConversionRatio), (int) (Double.valueOf(matcher.group(5)) * unitConversionRatio)),
new Double(matcher.group(6)).intValue());
apertureMacro.addPrimitive(centerLine);
return;
}
matcher = PATTERN_MACRO_1.matcher(str);
if (matcher.find())
{
MacroCircle circle = new MacroCircle((int) (Double.valueOf(matcher.group(2)) * unitConversionRatio),
new Point((int) (Double.valueOf(matcher.group(3)) * unitConversionRatio), (int) (Double.valueOf(matcher.group(4)) * unitConversionRatio)));
apertureMacro.addPrimitive(circle);
return;
}
matcher = PATTERN_MACRO_20.matcher(str);
if (matcher.find())
{
MacroVectorLine vectorLine = new MacroVectorLine((int) (Double.valueOf(matcher.group(2)) * unitConversionRatio),
new Point((int) (Double.valueOf(matcher.group(3)) * unitConversionRatio), (int)(Double.valueOf(matcher.group(4)) * unitConversionRatio)),
new Point((int) (Double.valueOf(matcher.group(5)) * unitConversionRatio), (int)(Double.valueOf(matcher.group(6)) * unitConversionRatio)),
(int)(Double.valueOf(matcher.group(7)).doubleValue()));
apertureMacro.addPrimitive(vectorLine);
return;
}
matcher = PATTERN_MACRO_4.matcher(str);
if (matcher.find())
{
int verticesCount = Integer.valueOf(matcher.group(2));
MacroOutline outline = new MacroOutline();
outline.setRotationAngle(new Double(matcher.group(4)).intValue());
matcher = PATTERN_MACRO_4_COORDINATE_PAIR.matcher(matcher.group(3));
while (matcher.find())
outline.addPoint(new Point((int) (Double.valueOf(matcher.group(1)) * unitConversionRatio), (int) (Double.valueOf(matcher.group(2)) * unitConversionRatio)));
if (verticesCount != outline.getPoints().size() - 1)
LoggerFactory.getApplicationLogger().log(Level.WARNING, "Aperture macro vertices count does not match supplied coordinates: " + str);
if (!outline.getPoints().get(0).equals(outline.getPoints().get(outline.getPoints().size() - 1)))
LoggerFactory.getApplicationLogger().log(Level.WARNING, "Aperture macro does not define enclosed area: " + str);
outline.getPoints().remove(outline.getPoints().size() - 1);
apertureMacro.addPrimitive(outline);
}
}
private void parseApertureDefinition(String str) throws GerberParsingException
{
if (!str.startsWith("D"))
throw new GerberParsingException("Invalid aperture definition: " + str);
str = str.substring(1);
Pattern pattern = Pattern.compile("(\\d+)(.*)");
Matcher matcher = pattern.matcher(str);
if (matcher.find())
{
ApertureMacro macro = apertureMacros.get(matcher.group(2));
if (macro != null)
{
apertures.put(Integer.valueOf(matcher.group(1)), macro);
return;
}
}
pattern = Pattern.compile("(\\d+)([CORP8]+)");
matcher = pattern.matcher(str);
if (!matcher.find())
throw new GerberParsingException("Aperture definition incorrectly formatted: " + str);
int apertureNumber = Integer.parseInt(matcher.group(1));
String aperture = matcher.group(2);
if (aperture.equals("C"))
{
pattern = Pattern.compile(".*,(\\d*.\\d+)");
matcher = pattern.matcher(str);
if (!matcher.find())
throw new GerberParsingException("Invalid definition of circular aperture");
int diameter = (int)(Double.valueOf(matcher.group(1)) * unitConversionRatio);
apertures.put(apertureNumber, new CircularAperture(diameter));
}
else if (aperture.equals("R"))
{
pattern = Pattern.compile(".*,(\\d*.\\d+)X(\\d*.\\d+)");
matcher = pattern.matcher(str);
if (!matcher.find())
throw new GerberParsingException("Invalid definition of rectangular aperture");
int width = (int)(Double.valueOf(matcher.group(1)) * unitConversionRatio);
int height = (int)(Double.valueOf(matcher.group(2)) * unitConversionRatio);
apertures.put(apertureNumber, new RectangularAperture(width, height));
}
else if (aperture.equals("OC8"))
{
pattern = Pattern.compile(".*,(\\d*.\\d+)");
matcher = pattern.matcher(str);
if (!matcher.find())
throw new GerberParsingException("Invalid definition of octagonal aperture");
int diameter = (int)(Double.valueOf(matcher.group(1)) * unitConversionRatio);
apertures.put(apertureNumber, new OctagonalAperture(diameter));
}
else if (aperture.equals("O"))
{
pattern = Pattern.compile(".*,(\\d*.\\d+)X(\\d*.\\d+)");
matcher = pattern.matcher(str);
if (!matcher.find())
throw new GerberParsingException("Invalid definition of oval aperture");
int width = (int) (Double.valueOf(matcher.group(1)) * unitConversionRatio);
int height = (int) (Double.valueOf(matcher.group(2)) * unitConversionRatio);
apertures.put(apertureNumber, new OvalAperture(width, height));
}
else if (aperture.equals("P"))
{
System.out.println("Polygon aperture");
}
else
throw new GerberParsingException("Unknown aperture");
}
private DataBlock parseDataBlock(String str)
{
DataBlock dataBlock = new DataBlock();
Pattern pattern = Pattern.compile("([GMDXYIJ])([+-]?\\d+)");
Matcher matcher = pattern.matcher(str);
int i = 0;
while (matcher.find(i))
{
switch (matcher.group(1).charAt(0))
{
case 'G': dataBlock.setG(Integer.parseInt(matcher.group(2))); break;
case 'M': dataBlock.setM(Integer.parseInt(matcher.group(2))); break;
case 'D': dataBlock.setD(Integer.parseInt(matcher.group(2))); break;
case 'X': dataBlock.setX(convertCoordinates(matcher.group(2))); break;
case 'Y': dataBlock.setY(convertCoordinates(matcher.group(2))); break;
case 'I': dataBlock.setI(convertCoordinates(matcher.group(2))); break;
case 'J': dataBlock.setJ(convertCoordinates(matcher.group(2))); break;
}
i = matcher.end();
}
return dataBlock;
}
private int convertCoordinates(String str)
{
boolean negative = str.startsWith("-");
if (str.startsWith("-") || str.startsWith("+"))
str = str.substring(1);
while (str.length() < integerPlaces + decimalPlaces)
str = omitLeadingZeros ? '0' + str : str + '0';
str = str.substring(0, integerPlaces) + "." + str.substring(integerPlaces, str.length());
return (int)(Double.valueOf(str) * unitConversionRatio) * (negative ? -1 : 1);
}
private void processDataBlock(DataBlock dataBlock) throws GerberParsingException
{
if (dataBlock.getG() != null)
{
switch (dataBlock.getG())
{
case 1:
currentInterpolationMode = InterpolationMode.LINEAR;
break;
case 2:
currentInterpolationMode = InterpolationMode.CLOCKWISE_CIRCULAR;
break;
case 3:
currentInterpolationMode = InterpolationMode.COUNTERCLOCKWISE_CIRCULAR;
break;
case 4: return;
case 36:
region = new Region(polarity);
break;
case 37:
if (!region.getSegments().isEmpty())
elements.add(region);
region = null;
break;
case 54: break;
case 70:
unitConversionRatio = INCHES_RATIO;
break;
case 71:
unitConversionRatio = MM_RATIO;
break;
case 74:
arcQuadrantMode = ArcQuadrantMode.SINGLE_QUADRANT;
break;
case 75:
arcQuadrantMode = ArcQuadrantMode.MULTI_QUADRANT;
break;
default:
throw new GerberParsingException("Unknown gcode: " + dataBlock.getG());
}
}
if (dataBlock.getM() != null)
{
switch (dataBlock.getM())
{
case 2: return;
default:
throw new GerberParsingException("Unknown mcode: " + dataBlock.getM());
}
}
if (dataBlock.getD() != null)
{
switch (dataBlock.getD())
{
case 1: exposureMode = ExposureMode.ON; break;
case 2: exposureMode = ExposureMode.OFF; break;
case 3: exposureMode = ExposureMode.FLASH; break;
default:
aperture = apertures.get(dataBlock.getD());
if (aperture == null)
throw new GerberParsingException("Undefined aperture used: " + dataBlock.getD());
return;
}
}
if (dataBlock.getX() == null && dataBlock.getY() == null && dataBlock.getD() == null)
return;
Integer newX = x;
if (dataBlock.getX() != null)
newX = dataBlock.getX();
Integer newY = y;
if (dataBlock.getY() != null)
newY = dataBlock.getY();
GerberPrimitive primitive = null;
if(exposureMode == ExposureMode.FLASH)
primitive = new Flash(newX, newY, aperture, polarity);
else if (exposureMode == ExposureMode.ON)
{
if (currentInterpolationMode == InterpolationMode.LINEAR)
primitive = new LinearShape(x, y, newX, newY, aperture, polarity);
else
{
Integer i = dataBlock.getI() == null ? 0 : dataBlock.getI();
Integer j = dataBlock.getJ() == null ? 0 : dataBlock.getJ();
primitive = new CircularShape(x, y, newX, newY, x + i, y + j,
currentInterpolationMode == InterpolationMode.CLOCKWISE_CIRCULAR, aperture, polarity);
}
}
if (region != null)
{
if (exposureMode == ExposureMode.ON && (!newX.equals(x) || !newY.equals(y)))
region.addSegment(primitive);
}
else if (aperture != null)
{
if (exposureMode == ExposureMode.FLASH || exposureMode == ExposureMode.ON)
elements.add(primitive);
}
x = newX;
y = newY;
}
}