def parse_gerber(filename):
	info("parsing gerber file \"" + filename + "\"...")
	#global units
	#global zero_suppression_mode
	#global coordinate_mode
	#global layer_polarity
	#global x_format
	#global y_format
	#global number_of_digits
	#global ratio
	global decimal_places # fixme - a bit funny if this changes between gerbers we're reading
	zero_suppression_mode = "leading"
	coordinate_mode = "absolute"
	layer_polarity = "dark"
	x_format = "55"
	y_format = "55"
	#units = "mm"
	lines = []
	for line in open(filename):
		line = line.rstrip("\n\r")
		lines.append(line)
	gerber_instructions = []
	global apertures
	apertures = {}
	aperture = "00"
	matched_units = 0
	matched_format = 0
	set_ratio = 0
	#gerber_instructions.append("hi there")
	for line in lines:
		matches = 0
		match = re.search("^%MO([IM][NM])\*%$", line)
		if match:
			matches = matches + 1
			if (match.group(1) == "IN"):
				#debug("set units to inches: " + line)
				ratio = 1000.0 / 25.4 / 1.55 # fixme/todo: 1.55 is a magic number here
			else:
				#debug("set units to mm: " + line)
				ratio = 1.0
			matched_units = 1
		match = re.search("^%FS([LTD])([AI])X([0-9][0-9])Y([0-9][0-9])\*%$", line)
		if match:
			matches = matches + 1
			debug("set format: " + line)
			if (match.group(1) == "L"):
				zero_suppression_mode = "leading"
			else:
				zero_suppression_mode = "trailing"
			if (match.group(2) == "A"):
				coordinate_mode = "absolute"
			else:
				coordinate_mode = "incremental"
			x_format = match.group(3)
			y_format = match.group(4)
			decimal_places = int(x_format[1])
			number_of_digits = int(x_format[0]) + int(x_format[1])
			number_of_digits = number_of_digits + 1 # some gerber files output 7 digits for "24" format...
			#decimal_places = decimal_places + 1
			debug("number of digits to use for coordinates = " + str(number_of_digits))
			matched_format = 1
		match = re.search("^%ADD([0-9]+)C,([.0-9]+)\*%$", line) # %ADD010C,0.0254*%
		if match:
			matches = matches + 1
			#aperture_length = len(match.group(1))
			ap = int(match.group(1))
			apertures[ap] = ("C", float(match.group(2)), float(match.group(2)))
			debug("aperture definition: " + line)
		match = re.search("^%ADD([0-9]+)R,([.0-9]+)X([.0-9]+)\*%$", line) # %ADD028R,2.6X1.6*% or %ADD34R,0.0138X0.0472*%
		if match:
			matches = matches + 1
			ap = int(match.group(1))
			apertures[ap] = ("R", float(match.group(2)), float(match.group(3)))
			debug("aperture definition: " + line)
		match = re.search("^%ADD([0-9]+)O,([.0-9]+)X([.0-9]+)\*%$", line) # %ADD11O,0.0138X0.0669*%
		if match:
			matches = matches + 1
			ap = int(match.group(1))
			apertures[ap] = ("O", float(match.group(2)), float(match.group(3)))
			debug("aperture definition: " + line)
		match = re.search("^%LN([a-zA-Z0-9]+)\*%$", line)
		if match:
			matches = matches + 1
			gerber_instructions.append("setlayer" + match.group(1))
		match = re.search("^(G0[1-3])\*$", line)
		if match:
			matches = matches + 1
			gerber_instructions.append(match.group(1))
		match = re.search("^(G0[1-3][XY].*)\*$", line)
		if match:
			matches = matches + 1
			gerber_instructions.append(match.group(1))
		match = re.search("^([XY].*)\*$", line)
		if match:
			matches = matches + 1
			#debug(line)
			gerber_instructions.append(match.group(1))
		match = re.search("^(G54D)([0-9]+)\*$", line)
		if match:
			matches = matches + 1
			debug("aperture selection: " + match.group(2))
			gerber_instructions.append(match.group(1) + match.group(2))
			#debug(match.group(1))
		match = re.search("^%LP([DC])\*%$", line)
		if match:
			# this unfortunately only grabs the last instance...
			matches = matches + 1
			if (match.group(1) == "D"):
				layer_polarity = "dark"
			else:
				layer_polarity = "clear"
		match = re.search("^%IPNEG\*%$", line) # %IPNEG% = reverse whole layer
		if match:
			matches = matches + 1
			# do something here for this...
		match = re.search("^%IPPOS\*%$", line) # %IPPOS% = whole layer normal
		if match:
			matches = matches + 1
		match = re.search("^G04.*$", line)
		if match:
			matches = matches + 1
		match = re.search("^%$", line)
		if match:
			matches = matches + 1
		match = re.search("^\*$", line)
		if match:
			matches = matches + 1
		match = re.search("^G74\*$", line)
		if match:
			warning("cannot handle single quadrant G74 mode")
			matches = matches + 1
			#exit()
		match = re.search("^G75\*$", line) # multi-quadrant mode
		if match:
			matches = matches + 1
		match = re.search("^$", line)
		if match:
			matches = matches + 1
		match = re.search("^M02\*$", line) # M02* = end of job
		if match:
			matches = matches + 1
		match = re.search("^%AM(.*)\*$", line) # AM = aperture macro definition
		if match:
			matches = matches + 1
			warning("ignoring aperture macro \"" + match.group(1) + "\"")
		match = re.search("^([$0-9]*),", line) # aperture macro definition continuation
		if match:
			matches = matches + 1
		match = re.search("^%AD(.*)\*$", line) # AD = aperture macro instantiation
		if match:
			matches = matches + 1
			error("aperture instantiation \"" + match.group(1) + "\" ignored - this part needs to be coded")
		match = re.search("^%IN(.*)\*%$", line) # IN = image name
		if match:
			matches = matches + 1
			debug("ignoring image name \"" + match.group(1) + "\"")
		if (matches == 0):
			warning("did not parse \"" + line + "\"")
		if (set_ratio == 0) and (matched_units == 1) and (matched_format == 1):
			ratio = ratio / (10.0**int(decimal_places))
			debug("ratio = " + str(ratio))
			gerber_instructions.append("setratio" + str(ratio))
			set_ratio = 1
	if (matched_format == 0):
		error("did not find format line", 2)
	for aperture in apertures:
		(CROP, w, h) = apertures[aperture]
		debug("aperture[" + str(aperture) + "]: " + CROP + " " + str(w) + " " + str(h))
	#debug("units: " + units)
	debug("x_format: " + x_format)
	debug("y_format: " + y_format)
	debug("layer_polarity: " + layer_polarity)
	debug("zero_suppression_mode: " + zero_suppression_mode)
	debug("coordinate_mode: " + coordinate_mode)
	return gerber_instructions
def draw_gerber_layer(parent_object, gerber_instructions, layer_name, color = "#000000", stroke_width = laser_stroke_width):
	layer = add_layer(parent_object, layer_name)
	overall_layer = layer
	group = add_group(layer, color=color, stroke_width=stroke_width)
	#group.stroke(color=color, width=float(stroke_width), miterlimit=4, opacity=1)
	#group.dasharray("none")
	#group.fill("none")
	debug2("instructions:")
	x = 0
	y = 0
	x_old = 0
	y_old = 0
	x_string = "0"
	y_string = "0"
	i_string = "0"
	d_string = "02" # pen up
	i = 0
	j = 0
	aperture = "00"
	mode = "linear"
	ratio = 5.0
	#global verbosity
	#verbosity = 4
	for instruction in gerber_instructions:
		debug("instruction: " + instruction)
		match = re.search("^setratio([.0-9]*[e]*[-0-9]*)$", instruction)
		if match:
			ratio = float(match.group(1))
			debug("found ratio: " + str(ratio))
		match = re.search("^setlayer(.*)$", instruction)
		if match:
			layer_name = match.group(1)
			layer = add_layer(overall_layer, layer_name)
			group = add_group(layer, color=color, stroke_width=stroke_width)
		match = re.search("^G54D([1-9][0-9])$", instruction) # "G54D22" means follow the goto X,Y instructions after this line, and flash the D22 aperture every time we get a D03 ("flash aperture") command
		if match:
			debug("aperture selection: " + match.group(1))
			aperture = int(match.group(1))
			if not aperture in apertures.keys():
				error("can't find aperture #" + str(aperture), 4)
			(CROP, w, h) = apertures[aperture]
			w = 25.4 * w # fixme/todo:  magic number here
			h = 25.4 * h # fixme/todo:  magic number here
			debug("aperture[" + str(aperture) + "]: " + CROP + " " + str(w) + " " + str(h))
			d_string = "04" # skip doing anything this pass through the following code
		match = re.search("^G(0[0-3])(.*)$", instruction)
		if match:
			instruction = match.group(2)
			#debug2("remaining instruction: " + instruction)
			if (match.group(1) == "01"):
				mode = "linear"
				debug("mode = linear")
			elif (match.group(1) == "02"):
				mode = "cc_arc"
				debug("mode = cc arc")
			elif (match.group(1) == "03"):
				mode = "cw_arc"
				debug("mode = cw arc")
			else:
				error("?", 3)
		match = 1
		while match:
			#match = re.search("^([XYIJ])([-0-9]{1," + str(number_of_digits) + "})(.*)$", instruction)
			match = re.search("^([XYIJ])([-0-9]{1,10})(.*)$", instruction)
			if match:
				instruction = match.group(3)
				#debug2("remaining instruction: " + instruction)
				if (match.group(1) == "X"):
					x_string = match.group(2)
					x = +int(x_string) * ratio
				elif (match.group(1) == "Y"):
					y_string = match.group(2)
					y = +int(y_string) * ratio
				elif (match.group(1) == "I"):
					i_string = match.group(2)
					i = +int(i_string) * ratio
				elif (match.group(1) == "J"):
					j_string = match.group(2)
					j = +int(j_string) * ratio
		match = re.search("^D(0[1-3])$", instruction)
		if match:
			d_string = match.group(1)
		if (d_string == "01"): # pen down
			debug(" " + x_string + " " + y_string + " " + d_string)
			if (mode == "linear"):
				group.add(svg.line( (x_old,y_old), (x,y) ))
				#debug("(" + str(x_old) + "," + str(y_old) + ") -> (" + str(x) + "," + str(y) + ") with pen down")
			else:
				# PADS: "G03X58250I-3315J-5D01*" or "G03X201969Y117031I0J-1969D01*"
				# altium: "G03*", then "X-1778Y-10160I2540J0D01*"
				radius = math.sqrt(i**2+j**2)
				delta_x = x - x_old
				delta_y = y - y_old
				x_center = x_old + i
				y_center = y_old + j
				debug("old(" + str(x_old) + "," + str(y_old) + ") -> xy(" + str(x) + "," + str(y) + ") -> ij(" + str(i) + "," + str(j) + ") -> center(" + str(x_center) + "," + str(y_center) + ")")
				#debug("(x,y,i,j) = (" + str(x) + "," + str(y) + "," + str(i) + "," + str(j) + ")")
				if (mode == "cc_arc"):
					#debug("cw arc here")
					# from http://stackoverflow.com/questions/25019441/arc-pie-cut-in-svgwrite
					# dwg.path(d="M {0},{1} l {2},{3} a {4},{4} 0 0,0 {5},{6} z".format(start_x, start_y, m0, n0, radius, m1, n1),
					# M=moveto, L=lineto, a=arc?, z=close path
					# a: (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
					string = "M {0},{1} a {2},{2} 0 0,0 {3},{4}".format(x_old, y_old, radius, delta_x, delta_y)
				elif (mode == "cw_arc"):
					string = "M {0},{1} a {2},{2} 0 0,1 {3},{4}".format(x_old, y_old, radius, delta_x, delta_y)
				else:
					error("?", 4)
				#group.add(svg.circle( (x_center,y_center), radius, fill="none" ))
				#group.add(svg.line( (x_old, y_old), (x, y) ))
				debug(string)
				group.add(svg.path(d=string, fill="none"))
		elif (d_string == "02"):
			pass
			# maybe change to a new group here?
		elif (d_string == "03"):
			if (aperture == 0):
				warning("unhandled D03 flash operation")
			else:
				t = laser_stroke_width / 2.0
				#x = x / 1.5
				#y = y / 1.5
				debug("flashing aperture[" + str(aperture) + "]: " + CROP + " " + str(w) + " " + str(h) + " at (" + str(x) + "," + str(y) + ")")
				if (CROP == "R"):
					group.add(svg.rect( (x-w/2.0+t,y-h/2.0+t), (w-2*t,h-2*t) ))
				# should add a special case here for O (oval) and make a rectangle and two half-circles
		elif (d_string == "04"):
			pass # silently fall thorough, otherwise G54D commands trigger output the first pass through
		else:
			error("unknown d string", 5)
		x_old = x
		y_old = y
	return overall_layer
panel_frame_thickness = 10.0 # mm - overall border thickness
#panel_tab_length = 7.0 # mm
#panel_tab_width = 2.0 # mm
x_gap_between_instances_of_boards = 5.0 # mm
y_gap_between_instances_of_boards = 5.0 # mm
#stencil_half_moon_location = "NorthSouth"
stencil_half_moon_location = "EastWest"
number_of_extra_half_moons_per_side = 1

protoboard_width  = 151 # should measure this width so that the mirror-image ends up in the right place
protoboard_height = 151
if (fill_protoboard == 1):
	number_of_horizontal_instances = int(math.floor(protoboard_width  / board_width))
	number_of_vertical_instances   = int(math.floor(protoboard_height / board_height))
	if (number_of_horizontal_instances < 1) or (number_of_vertical_instances < 1):
		error("can't fit board on panel", 6)
	panel_width = number_of_horizontal_instances * board_width
	panel_height = number_of_vertical_instances * board_height
	panel_width  = panel_width  + (number_of_horizontal_instances - 1) * x_gap_between_instances_of_boards 
	panel_height = panel_height + (number_of_horizontal_instances - 1) * y_gap_between_instances_of_boards 
	extra_x_offset = (protoboard_width  - panel_width ) / 2.0
	extra_y_offset = (protoboard_height - panel_height) / 2.0
	panel_width  = protoboard_width
	panel_height = protoboard_height
else:
	#extra_x_offset = x_gap_between_instances_of_boards # or 0.0/whatever
	#extra_y_offset = y_gap_between_instances_of_boards # or 0.0/whatever
	extra_x_offset = panel_frame_thickness # + panel_tab_length
	extra_y_offset = panel_frame_thickness
	panel_width = number_of_horizontal_instances * board_width + 2.0 * extra_x_offset
	panel_height = number_of_vertical_instances * board_height + 2.0 * extra_y_offset