2021-02-14 08:51:14 -08:00
|
|
|
#!/usr/bin/env python
|
|
|
|
############################################################################################
|
|
|
|
# Hellen-One: A board rendering script.
|
|
|
|
# (c) andreika <prometheus.pcb@gmail.com>
|
|
|
|
############################################################################################
|
|
|
|
|
|
|
|
import os, sys
|
|
|
|
|
|
|
|
sys.path.append("./bin/pcb-tools")
|
|
|
|
from gerber import PCB
|
|
|
|
from gerber.primitives import *
|
|
|
|
from gerber.render import theme
|
|
|
|
from gerber.render import RenderSettings
|
|
|
|
from gerber.render.cairo_backend import GerberCairoContext
|
|
|
|
|
|
|
|
if len(sys.argv) < 3:
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("Error! Please specify the gerber path, image filename and board side.")
|
2021-02-14 08:51:14 -08:00
|
|
|
sys.exit(1)
|
|
|
|
gerberPath = sys.argv[1]
|
|
|
|
imageFileName = sys.argv[2]
|
|
|
|
boardSide = sys.argv[3]
|
|
|
|
dpi = sys.argv[4]
|
|
|
|
|
|
|
|
# This class is needed only to store the board outline as a filled mask
|
|
|
|
class HellenGerberCairoContext(GerberCairoContext):
|
|
|
|
def __init__(self, scale=600):
|
|
|
|
super(HellenGerberCairoContext, self).__init__(scale)
|
|
|
|
self.lineList = []
|
|
|
|
|
|
|
|
# this override is needed to adjust the scale to correspond to the given max_width/max_height values
|
|
|
|
def render_layers(self, layers, filename, theme, verbose, dpi, isOutline):
|
|
|
|
scale = int(dpi)
|
|
|
|
self.isOutline = isOutline
|
|
|
|
self.scale = (scale, scale)
|
|
|
|
self.clear()
|
|
|
|
|
|
|
|
# Render layers
|
|
|
|
bgsettings = theme['background']
|
|
|
|
for layer in layers:
|
|
|
|
settings = theme.get(layer.layer_class, RenderSettings())
|
|
|
|
self.render_layer(layer, settings=settings, bgsettings=bgsettings,
|
|
|
|
verbose=verbose)
|
|
|
|
self.dump(filename, verbose)
|
|
|
|
|
2021-02-21 08:56:09 -08:00
|
|
|
def addPoint(self, start, end, additional):
|
|
|
|
start = (start[0] + self.origin_in_pixels[0], start[1] + self.origin_in_pixels[1])
|
|
|
|
end = (end[0] + self.origin_in_pixels[0], end[1] + self.origin_in_pixels[1])
|
|
|
|
self.lineList.append([start, end, additional])
|
|
|
|
|
2021-02-14 08:51:14 -08:00
|
|
|
# this override is needed to gather all line segments from the outline layers
|
|
|
|
def _render_line(self, line, color):
|
|
|
|
if self.isOutline == False or self._render_count != 1: # outline layer
|
|
|
|
GerberCairoContext._render_line(self, line, color)
|
|
|
|
return
|
|
|
|
start = self.scale_point(line.start)
|
|
|
|
end = self.scale_point(line.end)
|
|
|
|
# we don't draw lines, we store the line segments
|
|
|
|
with self._clip_primitive(line):
|
2021-02-21 08:56:09 -08:00
|
|
|
self.addPoint(start, end, None)
|
|
|
|
|
|
|
|
# this override is needed to gather all arcs from the outline layers
|
|
|
|
def _render_arc(self, arc, color):
|
|
|
|
if self.isOutline == False or self._render_count != 1: # outline layer
|
|
|
|
GerberCairoContext._render_arc(self, arc, color)
|
|
|
|
return
|
|
|
|
start = self.scale_point(arc.start)
|
|
|
|
end = self.scale_point(arc.end)
|
|
|
|
# we don't draw arcs, we store them
|
|
|
|
with self._clip_primitive(arc):
|
|
|
|
self.addPoint(start, end, arc)
|
2021-02-14 08:51:14 -08:00
|
|
|
|
2021-02-21 08:56:09 -08:00
|
|
|
# this override is needed to render the actual outline layers in a form of closed filled polygon
|
2021-02-14 08:51:14 -08:00
|
|
|
def flatten(self, color=None, alpha=None):
|
|
|
|
if self.isOutline == True and self._render_count == 1: # outline layer
|
|
|
|
# reconstruct the contour (the original lines go in no particular order)
|
|
|
|
curPoint = self.lineList[0]
|
|
|
|
countourPoints = []
|
|
|
|
for i in range(1000):
|
2021-02-21 08:56:09 -08:00
|
|
|
countourPoints.append([curPoint[0][0], curPoint[0][1], curPoint[2]])
|
2021-02-14 08:51:14 -08:00
|
|
|
dist = sys.float_info.max
|
|
|
|
# find the closest point among the
|
|
|
|
for ll in self.lineList:
|
2021-02-21 08:56:09 -08:00
|
|
|
llinv = [ll[1], ll[0], ll[2]]
|
|
|
|
if ll == curPoint or llinv == curPoint:
|
2021-02-14 08:51:14 -08:00
|
|
|
continue
|
|
|
|
d = (ll[0][0] - curPoint[1][0])**2 + (ll[0][1] - curPoint[1][1])**2
|
|
|
|
if d < dist:
|
|
|
|
dist = d
|
|
|
|
nextCurPoint = ll
|
|
|
|
d = (ll[1][0] - curPoint[1][0])**2 + (ll[1][1] - curPoint[1][1])**2
|
|
|
|
if d < dist:
|
|
|
|
dist = d
|
2021-02-21 08:56:09 -08:00
|
|
|
nextCurPoint = llinv
|
2021-02-14 08:51:14 -08:00
|
|
|
if dist < 1.e-4:
|
|
|
|
break
|
|
|
|
if nextCurPoint[0] in countourPoints:
|
|
|
|
break
|
|
|
|
curPoint = nextCurPoint
|
|
|
|
|
|
|
|
# draw the filled contour
|
|
|
|
with self._new_mask() as mask:
|
2021-02-21 08:56:09 -08:00
|
|
|
mask.ctx.move_to(countourPoints[0][0], countourPoints[0][1])
|
|
|
|
wasArc = False
|
2021-02-14 08:51:14 -08:00
|
|
|
for ll in countourPoints:
|
2021-02-21 08:56:09 -08:00
|
|
|
if ll[2]:
|
|
|
|
arc = ll[2]
|
|
|
|
center = self.scale_point(arc.center)
|
|
|
|
radius = self.scale[0] * arc.radius
|
|
|
|
two_pi = 2 * math.pi
|
|
|
|
angle1 = (arc.start_angle + two_pi) % two_pi
|
|
|
|
angle2 = (arc.end_angle + two_pi) % two_pi
|
|
|
|
if arc.direction == 'counterclockwise':
|
|
|
|
mask.ctx.arc(center[0], center[1], radius, angle1, angle2)
|
|
|
|
else:
|
|
|
|
mask.ctx.arc_negative(center[0], center[1], radius, angle1, angle2)
|
|
|
|
else:
|
|
|
|
mask.ctx.line_to(ll[0], ll[1])
|
2021-02-14 08:51:14 -08:00
|
|
|
mask.ctx.set_source_rgba(1.0, 1.0, 1.0, 1.0)
|
|
|
|
mask.ctx.fill()
|
|
|
|
self.ctx.mask_surface(mask.surface, 0.0, 0.0)
|
|
|
|
GerberCairoContext.flatten(self, color, alpha)
|
|
|
|
|
|
|
|
|
|
|
|
###################################################################
|
|
|
|
|
|
|
|
# read the gerbers
|
|
|
|
pcb = PCB.from_directory(gerberPath, None, verbose=True)
|
|
|
|
|
|
|
|
outlineTheme = theme.Theme(name='OutlineTheme',
|
|
|
|
background=RenderSettings(theme.COLORS['black'], alpha=0.0),
|
|
|
|
drill=RenderSettings(theme.COLORS['black'], alpha=1.0))
|
|
|
|
outlineTheme.outline = RenderSettings(theme.COLORS['white'], alpha=1.0)
|
|
|
|
|
|
|
|
jlcTheme = theme.Theme(name='JLC',
|
|
|
|
background=RenderSettings(theme.COLORS['fr-4']),
|
|
|
|
top=RenderSettings(theme.COLORS['hasl copper'], alpha=0.85),
|
|
|
|
bottom=RenderSettings(theme.COLORS['hasl copper'], alpha=0.85, mirror=True),
|
|
|
|
topmask=RenderSettings(theme.COLORS['green soldermask'], alpha=0.85, invert=True),
|
|
|
|
bottommask=RenderSettings(theme.COLORS['green soldermask'], alpha=0.85, invert=True, mirror=True),
|
|
|
|
topsilk=RenderSettings(theme.COLORS['white'], alpha=1.0),
|
|
|
|
bottomsilk=RenderSettings(theme.COLORS['white'], alpha=1.0, mirror=True),
|
|
|
|
drill=RenderSettings(theme.COLORS['black'], alpha=1.0))
|
|
|
|
|
|
|
|
curTheme = jlcTheme
|
|
|
|
|
|
|
|
# choose layers
|
|
|
|
if boardSide == 'top':
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("* Top Side:")
|
2021-02-14 08:51:14 -08:00
|
|
|
boardLayers = pcb.top_layers + pcb.drill_layers
|
|
|
|
isOutline = False
|
|
|
|
elif boardSide == 'bottom':
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("* Bottom Side:")
|
2021-02-14 08:51:14 -08:00
|
|
|
boardLayers = pcb.bottom_layers + pcb.drill_layers
|
|
|
|
isOutline = False
|
|
|
|
elif boardSide == 'outline':
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("* Board Outline:")
|
2021-02-14 08:51:14 -08:00
|
|
|
boardLayers = [pcb.outline_layer] + pcb.drill_layers
|
|
|
|
curTheme = outlineTheme
|
|
|
|
isOutline = True
|
|
|
|
else:
|
2022-03-26 12:30:24 -07:00
|
|
|
print ("Error! Please specify the valid board side.")
|
2021-02-14 08:51:14 -08:00
|
|
|
sys.exit(2)
|
|
|
|
|
|
|
|
# render
|
|
|
|
ctx = HellenGerberCairoContext()
|
|
|
|
ctx.render_layers(layers=boardLayers, filename=imageFileName, theme=curTheme, verbose=True, dpi=dpi, isOutline=isOutline)
|