Merge old gerbolyze history

wip
jaseg 2021-01-30 20:19:34 +01:00
commit c5f8416b63
58 zmienionych plików z 30836 dodań i 0 usunięć

2
.gitignore vendored
Wyświetl plik

@ -1 +1,3 @@
build
dist
gerbolyze.egg-info

1
MANIFEST.in 100644
Wyświetl plik

@ -0,0 +1 @@
include README.rst

154
README.rst 100644
Wyświetl plik

@ -0,0 +1,154 @@
Gerbolyze high-resolution image-to-PCB converter
================================================
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample1.jpg
Tooling for PCB art is quite limited in both open source and closed source ecosystems. Something as simple as putting a
pretty picture on a PCB can be an extremely tedious task. Depending on the PCB tool used, various arcane incantations
may be necessary and even modestly complex images will slow down most PCB tools to a crawl.
Gerbolyze solves this problem in a toolchain-agnostic way by directly vectorizing bitmap files onto existing gerber
layers. Gerbolyze has been tested against both the leading open-source KiCAD toolchain and the industry-standard Altium
Designer. Gerbolyze is written with performance in mind and will happily vectorize tens of thousands of primitives,
generating tens of megabytes of gerber code without crapping itself. With gerbolyze you can finally be confident that
your PCB fab's toolchain will fall over before yours does if you overdo it with the high-poly anime silkscreen.
.. contents::
Produce high-quality artistic PCBs in three easy steps!
-------------------------------------------------------
Gerbolyze works in three steps.
1. Generate a scale-accurate preview of the finished PCB from your CAD tool's gerber output:
.. code::
$ gerbolyze render top my_gerber_dir preview.png
2. Load the resulting preview image into the GIMP or another image editing program. Use it as a guide to position scale
your artwork. Create a black-and-white image from your scaled artwork using GIMP's newsprint filter. Make sure most
details are larger than about 10px to ensure manufacturing goes smooth.
3. Vectorize the resulting grayscale image drectly into the PCB's gerber files:
.. code::
$ gerbolyze vectorize top input_gerber_dir output_gerber_dir black_and_white_artwork.png
Image preprocessing guide
-------------------------
Nice black-and-white images can be generated from any grayscale image using the GIMP's newsprint filter. The
straight-forward pre-processing steps necessary for use by ``gerbolyze vectorize`` are as follows.
1 Import a render of the board generated using ``gerbolyze render``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``gerbolyze render`` will automatically scale the render such that ten pixels in the render correspond to 6mil on the
board, which is about the smallest detail most manufacturers can resolve on the silkscreen layer. You can control this
setting using the ``--fab-resolution`` and ``--oversampling`` options. Refer to ``gerbolyze --help`` for details.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/01import01.png
2 Import your desired artwork
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Though anime or manga pictures are highly recommended, you can use any image including photographs. Be careful to select
a picture with comparatively low detail that remains recognizable at very low resolution. While working on a screen this
is hard to vizualize, but the grain resulting from the low resolution of a PCB's silkscreen is quite coarse.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/02import02.png
3 Paste the artwork onto the render as a new layer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/03paste.png
4 Scale, rotate and position the artwork to the desired size
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/04scale_cut.png
For alignment it may help to set the artwork layer's mode in the layers dialog to ``overlay``, which makes the PCB
render layer below shine through more. If you can't set the layer's mode, make sure you have actually made a new layer
from the floating selection you get when pasting one image into another in the GIMP.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/05position.png
5 Convert the image to grayscale
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/06grayscale.png
6 Fine-tune the image's contrast
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
To look well on the PCB, contrast is critical. If your source image is in color, you may have lost some contrast during
grayscale conversion. Now is the time to retouch that using the GIMP's color curve tool.
When using the GIMP's newsprint filter, bright grays close to white and dark grays close to black will cause very small
dots that might be beyond your PCB manufacturer's maximum resolution. To control this case, add small steps at the ends
of the grayscale value curve as shown (exaggerated) in the picture below. These steps saturate very bright grays to
white and very dark grays to black while preserving the values in the middle.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/08curve_cut.png
7 Retouch details
~~~~~~~~~~~~~~~~~
Therer might be small details that don't look right yet, such as the image's background color or small highlights that
merge into the background now. You can manually change the color of any detail now using the GIMP's flood-fill tool.
If you don't want the image's background to show up on the final PCB at all, just make it black.
Particularly on low-resolution source images it may make sense to apply a blur with a radius similar to the following
newsprint filter's cell size (10px) to smooth out the dot pattern generated by the newsprint filter.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/09retouch.png
In the following example, I retouched the highlights in the hair of the character in the picture to make them completely
white instead of light-gray, so they still stand out nicely in the finished picture.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/10retouched.png
8 Run the newsprint filter
~~~~~~~~~~~~~~~~~~~~~~~~~~
Now, run the GIMP's newsprint filter, under filters, distorts, newsprint.
The first important settings is the spot size, which should be larger than your PCB's minimum detail size (about 10px
with ``gerbolyze render`` default settings for good-quality silkscreen). In general the cheap and fast standard option of chinese PCB houses will require a larger detail size, but when you order specialty options like large size, 4-layer or non-green color along with a longer turnaround time you'll get much better-quality silk screen.
The second important setting is oversampling, which should be set to four or slightly higher. This improves the result
of the edge reconstruction of ``gerbolyze vectorize``.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/11newsprint.png
The following are examples on the detail resulting from the newsprint filter.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/12newsprint.png
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/13newsprint.png
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14newsprint.png
9 Export the image for use with ``gerbolyze vectorize``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Simply export the image as a PNG file. Below are some pictures of the output ``gerbolyze vectorize`` produced for this
example.
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/14result_cut.png
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/15result_cut.png
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/screenshots/16result_cut.png
Gallery
-------
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample2.jpg
.. image:: https://raw.githubusercontent.com/jaseg/gerbolyze/master/sample3.jpg

Wyświetl plik

@ -0,0 +1,35 @@
#!/usr/bin/env python3
import gerbolyze
if __name__ == '__main__':
# Parse command line arguments
import argparse
parser = argparse.ArgumentParser()
subcommand = parser.add_subparsers(help='Sub-commands')
subcommand.required, subcommand.dest = True, 'command'
vectorize_parser = subcommand.add_parser('vectorize', help='Vectorize bitmap image onto gerber layer')
render_parser = subcommand.add_parser('render', help='Render bitmap preview of board suitable as a template for positioning and scaling the input image')
parser.add_argument('-d', '--debugdir', type=str, default=None, help='Directory to place intermediate images into for debuggin')
vectorize_parser.add_argument('side', choices=['top', 'bottom'], help='Target board side')
vectorize_parser.add_argument('--layer', '-l', choices=['silk', 'mask', 'copper'], default='silk', help='Target layer on given side')
vectorize_parser.add_argument('source', help='Source gerber directory')
vectorize_parser.add_argument('target', help='Target gerber directory')
vectorize_parser.add_argument('image', help='Image to render')
render_parser.add_argument('--fab-resolution', '-r', type=float, nargs='?', default=6.0, help='Smallest feature size supported by PCB manufacturer, in mil. On silkscreen layers, this is the minimum font stroke width.')
render_parser.add_argument('--oversampling', '-o', type=float, nargs='?', default=10, help='Oversampling factor for the image. If set to say, 10 pixels, one minimum feature size (see --fab-resolution) will be 10 pixels long. The input image for vectorization should not contain any detail of smaller pixel size than this number in order to be manufacturable.')
render_parser.add_argument('side', choices=['top', 'bottom'], help='Target board side')
render_parser.add_argument('source', help='Source gerber directory')
render_parser.add_argument('image', help='Output image filename')
args = parser.parse_args()
if args.command == 'vectorize':
gerbolyze.process_gerbers(args.source, args.target, args.image, args.side, args.layer, args.debugdir)
else: # command == render
gerbolyze.render_preview(args.source, args.image, args.side, args.fab_resolution, args.oversampling)

Wyświetl plik

@ -0,0 +1,372 @@
#!/usr/bin/env python3
import tempfile
import os.path as path
import os
import sys
import time
import shutil
import math
import gerber
from gerber.render.cairo_backend import GerberCairoContext
import numpy as np
import cv2
import enum
import tqdm
def generate_mask(
outline,
target,
scale,
bounds,
debugimg,
status_print,
extend_overlay_r_mil,
subtract_gerber
):
# Render all gerber layers whose features are to be excluded from the target image, such as board outline, the
# original silk layer and the solder paste layer to binary images.
with tempfile.TemporaryDirectory() as tmpdir:
img_file = path.join(tmpdir, 'target.png')
status_print('Combining keepout composite')
fg, bg = gerber.render.RenderSettings((1, 1, 1)), gerber.render.RenderSettings((0, 0, 0))
ctx = GerberCairoContext(scale=scale)
status_print(' * outline')
ctx.render_layer(outline, settings=fg, bgsettings=bg, bounds=bounds)
status_print(' * target layer')
ctx.render_layer(target, settings=fg, bgsettings=bg, bounds=bounds)
for fn, sub in subtract_gerber:
status_print(' * extra layer', os.path.basename(fn))
layer = gerber.loads(sub)
ctx.render_layer(layer, settings=fg, bgsettings=bg, bounds=bounds)
status_print('Rendering keepout composite')
ctx.dump(img_file)
# Vertically flip exported image
original_img = cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)[::-1, :]
f = 1 if outline.units == 'inch' else 25.4
r = 1+2*max(1, int(extend_overlay_r_mil/1000 * f * scale))
status_print('Expanding keepout composite by', r)
# Extend image by a few pixels and flood-fill from (0, 0) to mask out the area outside the outermost outline
# This ensures no polygons are generated outside the board even for non-rectangular boards.
border = 10
outh, outw = original_img.shape
extended_img = np.zeros((outh + 2*border, outw + 2*border), dtype=np.uint8)
extended_img[border:outh+border, border:outw+border] = original_img
debugimg(extended_img, 'outline')
cv2.floodFill(extended_img, None, (0, 0), (255,))
original_img = extended_img[border:outh+border, border:outw+border]
debugimg(extended_img, 'flooded')
# Dilate the white areas of the image using gaussian blur and threshold. Use these instead of primitive dilation
# here for their non-directionality.
target_img = cv2.blur(original_img, (r, r))
_, target_img = cv2.threshold(target_img, 255//(1+r), 255, cv2.THRESH_BINARY)
return target_img
def render_gerbers_to_image(*gerbers, scale, bounds=None):
with tempfile.TemporaryDirectory() as tmpdir:
img_file = path.join(tmpdir, 'target.png')
fg, bg = gerber.render.RenderSettings((1, 1, 1)), gerber.render.RenderSettings((0, 0, 0))
ctx = GerberCairoContext(scale=scale)
for grb in gerbers:
ctx.render_layer(grb, settings=fg, bgsettings=bg, bounds=bounds)
ctx.dump(img_file)
# Vertically flip exported image to align coordinate systems
return cv2.imread(img_file, cv2.IMREAD_GRAYSCALE)[::-1, :]
def pcb_area_mask(outline, scale, bounds):
# Merge layers to target mask
img = render_gerbers_to_image(outline, scale=scale, bounds=bounds)
# Extend
imgh, imgw = img.shape
img_ext = np.zeros(shape=(imgh+2, imgw+2), dtype=np.uint8)
img_ext[1:-1, 1:-1] = img
# Binarize
img_ext[img_ext < 128] = 0
img_ext[img_ext >= 128] = 255
# Flood-fill
cv2.floodFill(img_ext, None, (0, 0), (255,)) # Flood-fill with white from top left corner (0,0)
img_ext_snap = img_ext.copy()
cv2.floodFill(img_ext, None, (0, 0), (0,)) # Flood-fill with black
cv2.floodFill(img_ext, None, (0, 0), (255,)) # Flood-fill with white
return np.logical_xor(img_ext_snap, img_ext)[1:-1, 1:-1].astype(float)
def generate_template(
silk, mask, copper, outline, drill,
image,
process_resolution:float=6, # mil
resolution_oversampling:float=10, # times
status_print=lambda *args:None
):
silk, mask, copper, outline, *drill = map(gerber.load_layer_data, [silk, mask, copper, outline, *drill])
silk.layer_class = 'topsilk'
mask.layer_class = 'topmask'
copper.layer_class = 'top'
outline.layer_class = 'outline'
f = 1.0 if outline.cam_source.units == 'metric' else 25.4
scale = (1000/process_resolution) / 25.4 * resolution_oversampling * f # dpmm
bounds = outline.cam_source.bounding_box
# Create a new drawing context
ctx = GerberCairoContext(scale=scale)
ctx.render_layer(outline, bounds=bounds)
ctx.render_layer(copper, bounds=bounds)
ctx.render_layer(mask, bounds=bounds)
ctx.render_layer(silk, bounds=bounds)
for dr in drill:
ctx.render_layer(dr, bounds=bounds)
ctx.dump(image)
def paste_image(
target_gerber:str,
outline_gerber:str,
source_img:np.ndarray,
subtract_gerber:list=[],
extend_overlay_r_mil:float=6,
extend_picture_r_mil:float=2,
status_print=lambda *args:None,
debugdir:str=None):
debugctr = 0
def debugimg(img, name):
nonlocal debugctr
if debugdir:
cv2.imwrite(path.join(debugdir, '{:02d}{}.png'.format(debugctr, name)), img)
debugctr += 1
# Parse outline layer to get bounds of gerber file
status_print('Parsing outline gerber')
outline = gerber.loads(outline_gerber)
bounds = (minx, maxx), (miny, maxy) = outline.bounding_box
grbw, grbh = maxx - minx, maxy - miny
status_print(' * outline has offset {}, size {}'.format((minx, miny), (grbw, grbh)))
# Parse target layer
status_print('Parsing target gerber')
target = gerber.loads(target_gerber)
(tminx, tmaxx), (tminy, tmaxy) = target.bounding_box
status_print(' * target layer has offset {}, size {}'.format((tminx, tminy), (tmaxx-tminx, tmaxy-tminy)))
# Read source image
imgh, imgw = source_img.shape
scale = math.ceil(max(imgw/grbw, imgh/grbh)) # scale is in dpmm
status_print(' * source image has size {}, going for scale {}dpmm'.format((imgw, imgh), scale))
# Merge layers to target mask
target_img = generate_mask(outline, target, scale, bounds, debugimg, status_print, extend_overlay_r_mil, subtract_gerber)
# Threshold source image. Ideally, the source image is already binary but in case it's not, or in case it's not
# exactly binary (having a few very dark or very light grays e.g. due to JPEG compression) we're thresholding here.
status_print('Thresholding source image')
qr = 1+2*max(1, int(extend_picture_r_mil/1000 * scale))
source_img = source_img[::-1]
_, source_img = cv2.threshold(source_img, 127, 255, cv2.THRESH_BINARY)
debugimg(source_img, 'thresh')
# Pad image to size of target layer images generated above. After this, `scale` applies to the padded image as well
# as the gerber renders. For padding, zoom or shrink the image to completely fit the gerber's rectangular bounding
# box. Center the image vertically or horizontally if it has a different aspect ratio.
status_print('Padding source image')
tgth, tgtw = target_img.shape
padded_img = np.zeros(shape=target_img.shape, dtype=source_img.dtype)
offx = int((minx-tminx if tminx < minx else 0)*scale)
offy = int((miny-tminy if tminy < miny else 0)*scale)
offx += int(grbw*scale - imgw) // 2
offy += int(grbh*scale - imgh) // 2
endx, endy = min(offx+imgw, tgtw), min(offy+imgh, tgth)
print('off', (offx, offy), 'end', (endx, endy), 'img', (imgw, imgh), 'tgt', (tgtw, tgth))
padded_img[offy:endy, offx:endx] = source_img[:endy-offy, :endx-offx]
debugimg(padded_img, 'padded')
debugimg(target_img, 'target')
# Mask out excluded gerber features (source silk, holes, solder mask etc.) from the target image
status_print('Masking source image')
out_img = (np.multiply((padded_img/255.0), (target_img/255.0) * -1 + 1) * 255).astype(np.uint8)
debugimg(out_img, 'multiplied')
# Calculate contours from masked target image and plot them to the target gerber context
status_print('Calculating contour lines')
plot_contours(out_img,
target,
offx=(minx, miny),
scale=scale,
status_print=lambda *args: status_print(' ', *args))
# Write target gerber context to disk
status_print('Generating output gerber')
from gerber.render import rs274x_backend
ctx = rs274x_backend.Rs274xContext(target.settings)
target.render(ctx)
out = ctx.dump().getvalue()
status_print('Done.')
return out
def plot_contours(
img:np.ndarray,
layer:gerber.rs274x.GerberFile,
offx:tuple,
scale:float,
debug=lambda *args:None,
status_print=lambda *args:None):
from gerber.primitives import Line, Region, Circle
imgh, imgw = img.shape
# Extract contour hierarchy using OpenCV
status_print('Extracting contours')
# See https://stackoverflow.com/questions/48291581/how-to-use-cv2-findcontours-in-different-opencv-versions/48292371
contours, hierarchy = cv2.findContours(img, cv2.RETR_TREE, cv2.CHAIN_APPROX_TC89_KCOS)[-2:]
aperture = list(layer.apertures)[0] if layer.apertures else Circle(None, 0.10)
status_print('offx', offx, 'scale', scale)
xbias, ybias = offx
def map(coord):
x, y = coord
return (x/scale + xbias, y/scale + ybias)
def contour_lines(c):
return [ Line(map(start), map(end), aperture, units=layer.settings.units)
for start, end in zip(c, np.vstack((c[1:], c[:1]))) ]
done = []
process_stack = [-1]
next_process_stack = []
parents = [ (i, first_child != -1, parent) for i, (_1, _2, first_child, parent) in enumerate(hierarchy[0]) ]
is_dark = True
status_print('Converting contours to gerber primitives')
with tqdm.tqdm(total=len(contours)) as progress:
while len(done) != len(contours):
for i, has_children, parent in parents[:]:
if parent in process_stack:
contour = contours[i]
polarity = 'dark' if is_dark else 'clear'
debug('rendering {} with parent {} as {} with {} vertices'.format(i, parent, polarity, len(contour)))
debug('process_stack is', process_stack)
debug()
layer.primitives.append(Region(contour_lines(contour[:,0]), level_polarity=polarity, units=layer.settings.units))
if has_children:
next_process_stack.append(i)
done.append(i)
parents.remove((i, has_children, parent))
progress.update(1)
debug('skipping to next level')
process_stack, next_process_stack = next_process_stack, []
is_dark = not is_dark
debug('done', done)
# Utility foo
# ===========
def find_gerber_in_dir(dir_path, extensions, exclude=''):
contents = os.listdir(dir_path)
exts = extensions.split('|')
excs = exclude.split('|')
for entry in contents:
if any(entry.lower().endswith(ext.lower()) for ext in exts) and not any(entry.lower().endswith(ex) for ex in excs if exclude):
lname = path.join(dir_path, entry)
if not path.isfile(lname):
continue
with open(lname, 'r') as f:
return lname, f.read()
raise ValueError(f'Cannot find file with suffix {extensions} in dir {dir_path}')
# Gerber file name extensions for Altium/Protel | KiCAD | Eagle
LAYER_SPEC = {
'top': {
'paste': '.gtp|-F_Paste.gbr|-F.Paste.gbr|.pmc',
'silk': '.gto|-F_SilkS.gbr|-F.SilkS.gbr|.plc',
'mask': '.gts|-F_Mask.gbr|-F.Mask.gbr|.stc',
'copper': '.gtl|-F_Cu.gbr|-F.Cu.gbr|.cmp',
'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb',
},
'bottom': {
'paste': '.gbp|-B_Paste.gbr|-B.Paste.gbr|.pms',
'silk': '.gbo|-B_SilkS.gbr|-B.SilkS.gbr|.pls',
'mask': '.gbs|-B_Mask.gbr|-B.Mask.gbr|.sts',
'copper': '.gbl|-B_Cu.gbr|-B.Cu.gbr|.sol',
'outline': '.gko|.gm1|-Edge_Cuts.gbr|-Edge.Cuts.gbr|.gmb'
},
}
# Command line interface
# ======================
def process_gerbers(source, target, image, side, layer, debugdir):
if not os.path.isdir(source):
raise ValueError(f'Given source "{source}" is not a directory.')
# Load input files
source_img = cv2.imread(image, cv2.IMREAD_GRAYSCALE)
if source_img is None:
print(f'"{image}" is not a valid image file', file=sys.stderr)
sys.exit(1)
tlayer, slayer = {
'silk': ('silk', 'mask'),
'mask': ('mask', 'silk'),
'copper': ('copper', None)
}[layer]
layers = LAYER_SPEC[side]
tname, tgrb = find_gerber_in_dir(source, layers[tlayer])
print('Target layer file {}'.format(os.path.basename(tname)))
oname, ogrb = find_gerber_in_dir(source, layers['outline'])
print('Outline layer file {}'.format(os.path.basename(oname)))
subtract = find_gerber_in_dir(source, layers[slayer]) if slayer else None
# Prepare output. Do this now to error out as early as possible if there's a problem.
if os.path.exists(target):
if os.path.isdir(target) and sorted(os.listdir(target)) == sorted(os.listdir(source)):
shutil.rmtree(target)
else:
print('Error: Target already exists and does not look like source. Please manually remove the target dir before proceeding.', file=sys.stderr)
sys.exit(1)
# Generate output
out = paste_image(tgrb, ogrb, source_img, [subtract], debugdir=debugdir, status_print=lambda *args: print(*args, flush=True))
shutil.copytree(source, target)
with open(os.path.join(target, os.path.basename(tname)), 'w') as f:
f.write(out)
def render_preview(source, image, side, process_resolution, resolution_oversampling):
def load_layer(layer):
name, grb = find_gerber_in_dir(source, LAYER_SPEC[side][layer])
print(f'{layer} layer file {os.path.basename(name)}')
return grb
outline = load_layer('outline')
silk = load_layer('silk')
mask = load_layer('mask')
copper = load_layer('copper')
try:
nm, npth = find_gerber_in_dir(source, '-npth.drl')
print(f'npth drill file {nm}')
except ValueError:
npth = None
nm, drill = find_gerber_in_dir(source, '.drl|.txt', exclude='-npth.drl')
print(f'drill file {nm}')
drill = ([npth] if npth else []) + [drill]
generate_template(
silk, mask, copper, outline, drill,
image,
process_resolution=process_resolution,
resolution_oversampling=resolution_oversampling,
)

357
gerboweb/colors.svg 100644
Wyświetl plik

@ -0,0 +1,357 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="210mm"
height="297mm"
viewBox="0 0 210 297"
version="1.1"
id="svg8"
sodipodi:docname="colors.svg"
inkscape:version="0.92.3 (2405546, 2018-03-11)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.98994949"
inkscape:cx="192.41904"
inkscape:cy="301.38284"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1030"
inkscape:window-x="0"
inkscape:window-y="50"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1">
<rect
style="fill:#006130;fill-opacity:1;stroke-width:0.1581243"
id="rect3713"
width="49.696209"
height="30.26951"
x="31.182737"
y="80.797615" />
<rect
style="fill:#00964a;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3"
width="49.696209"
height="30.26951"
x="31.182737"
y="111.06712" />
<rect
style="fill:#00d167;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6"
width="49.696209"
height="30.26951"
x="31.182737"
y="141.33664" />
<rect
style="fill:#4cffa4;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7"
width="49.696209"
height="30.26951"
x="31.182737"
y="171.60614" />
<rect
style="fill:#b7ffda;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5"
width="49.696209"
height="30.26951"
x="31.182737"
y="201.87566" />
<rect
style="fill:#e1fff0;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5-3"
width="49.696209"
height="30.26951"
x="31.182737"
y="232.14517" />
<rect
style="fill:#003018;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-5"
width="49.696209"
height="30.26951"
x="31.182737"
y="50.528107" />
<rect
style="fill:#003761;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-6"
width="49.696209"
height="30.26951"
x="80.878944"
y="80.797615" />
<rect
style="fill:#005596;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-2"
width="49.696209"
height="30.26951"
x="80.878944"
y="111.06712" />
<rect
style="fill:#0076d1;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-9"
width="49.696209"
height="30.26951"
x="80.878944"
y="141.33664" />
<rect
style="fill:#4cb1ff;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-1"
width="49.696209"
height="30.26951"
x="80.878944"
y="171.60614" />
<rect
style="fill:#b7e0ff;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5-2"
width="49.696209"
height="30.26951"
x="80.878944"
y="201.87566" />
<rect
style="fill:#e1f2ff;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5-3-7"
width="49.696209"
height="30.26951"
x="80.878944"
y="232.14517" />
<rect
style="fill:#001b30;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-5-0"
width="49.696209"
height="30.26951"
x="80.878944"
y="50.528107" />
<rect
style="fill:#611200;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-6-9"
width="49.696209"
height="30.26951"
x="130.57515"
y="80.797615" />
<rect
style="fill:#961c00;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-2-3"
width="49.696209"
height="30.26951"
x="130.57515"
y="111.06712" />
<rect
style="fill:#d12700;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-9-6"
width="49.696209"
height="30.26951"
x="130.57515"
y="141.33664" />
<rect
style="fill:#ff6e4c;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-1-0"
width="49.696209"
height="30.26951"
x="130.57515"
y="171.60614" />
<rect
style="fill:#ffc5b7;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5-2-6"
width="49.696209"
height="30.26951"
x="130.57515"
y="201.87566" />
<rect
style="fill:#ffe7e1;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5-3-7-2"
width="49.696209"
height="30.26951"
x="130.57515"
y="232.14517" />
<rect
style="fill:#300900;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-5-0-6"
width="49.696209"
height="30.26951"
x="130.57515"
y="50.528107" />
<flowRoot
xml:space="preserve"
id="flowRoot4715"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
transform="matrix(0.26458333,0,0,0.26458333,1.7165056,1.561349)"><flowRegion
id="flowRegion4717"><rect
id="rect4719"
width="117.1777"
height="112.12693"
x="65.659912"
y="221.46361" /></flowRegion><flowPara
id="flowPara4721">1</flowPara></flowRoot> <flowRoot
transform="matrix(0.26458333,0,0,0.26458333,1.9516334,31.90063)"
xml:space="preserve"
id="flowRoot4715-1"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion4717-8"><rect
id="rect4719-7"
width="117.1777"
height="112.12693"
x="65.659912"
y="221.46361" /></flowRegion><flowPara
id="flowPara4721-9">2</flowPara><flowPara
id="flowPara4747" /></flowRoot> <flowRoot
transform="matrix(0.26458333,0,0,0.26458333,1.8301938,62.095201)"
xml:space="preserve"
id="flowRoot4715-1-2"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion4717-8-0"><rect
id="rect4719-7-2"
width="117.1777"
height="112.12693"
x="65.659912"
y="221.46361" /></flowRegion><flowPara
id="flowPara4747-7">3</flowPara></flowRoot> <flowRoot
transform="matrix(0.26458333,0,0,0.26458333,1.8482805,92.369888)"
xml:space="preserve"
id="flowRoot4715-1-5"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion4717-8-9"><rect
id="rect4719-7-22"
width="117.1777"
height="112.12693"
x="65.659912"
y="221.46361" /></flowRegion><flowPara
id="flowPara4747-9">4</flowPara></flowRoot> <flowRoot
transform="matrix(0.26458333,0,0,0.26458333,1.8637835,122.56446)"
xml:space="preserve"
id="flowRoot4715-1-7"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion4717-8-3"><rect
id="rect4719-7-6"
width="117.1777"
height="112.12693"
x="65.659912"
y="221.46361" /></flowRegion><flowPara
id="flowPara4747-2">5</flowPara></flowRoot> <flowRoot
transform="matrix(0.26458333,0,0,0.26458333,1.7733497,152.90373)"
xml:space="preserve"
id="flowRoot4715-1-9"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion4717-8-31"><rect
id="rect4719-7-9"
width="117.1777"
height="112.12693"
x="65.659912"
y="221.46361" /></flowRegion><flowPara
id="flowPara4747-78">6</flowPara></flowRoot> <flowRoot
transform="matrix(0.26458333,0,0,0.26458333,1.82761,183.17841)"
xml:space="preserve"
id="flowRoot4715-1-4"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion4717-8-5"><rect
id="rect4719-7-0"
width="117.1777"
height="112.12693"
x="65.659912"
y="221.46361" /></flowRegion><flowPara
id="flowPara4747-1">7</flowPara></flowRoot> <flowRoot
xml:space="preserve"
id="flowRoot4842"
style="fill:black;fill-opacity:1;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;letter-spacing:0px;word-spacing:0px"><flowRegion
id="flowRegion4844"><rect
id="rect4846"
width="118.18785"
height="803.07129"
x="-4.0406103"
y="164.89507" /></flowRegion><flowPara
id="flowPara4848"></flowPara></flowRoot> <rect
style="fill:#611200;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-6-9-6"
width="49.696209"
height="30.26951"
x="130.57515"
y="80.797615" />
<rect
style="fill:#961c00;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-2-3-3"
width="49.696209"
height="30.26951"
x="130.57515"
y="111.06712" />
<rect
style="fill:#d12700;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-9-6-2"
width="49.696209"
height="30.26951"
x="130.57515"
y="141.33664" />
<rect
style="fill:#ff6e4c;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-1-0-0"
width="49.696209"
height="30.26951"
x="130.57515"
y="171.60614" />
<rect
style="fill:#ffc5b7;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5-2-6-6"
width="49.696209"
height="30.26951"
x="130.57515"
y="201.87566" />
<rect
style="fill:#ffe7e1;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5-3-7-2-1"
width="49.696209"
height="30.26951"
x="130.57515"
y="232.14517" />
<rect
style="fill:#300900;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-5-0-6-5"
width="49.696209"
height="30.26951"
x="130.57515"
y="50.528107" />
<rect
style="fill:#f5fbff;fill-opacity:1;stroke-width:0.1581243"
id="rect3713-3-6-7-5-3-7-5"
width="49.696209"
height="30.26951"
x="80.878944"
y="262.41467" />
<flowRoot
transform="matrix(0.26458333,0,0,0.26458333,1.8276101,213.44792)"
xml:space="preserve"
id="flowRoot4715-1-4-4"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"><flowRegion
id="flowRegion4717-8-5-7"><rect
id="rect4719-7-0-6"
width="117.1777"
height="112.12693"
x="65.659912"
y="221.46361" /></flowRegion><flowPara
id="flowPara4747-1-5">8</flowPara></flowRoot> </g>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 12 KiB

Wyświetl plik

@ -0,0 +1,4 @@
MAX_CONTENT_LENGTH=10000000
SECRET_KEY="FIXME: CHANGE THIS KEY"
UPLOAD_PATH="/var/cache/gerboweb/upload"
JOB_QUEUE_DB="/var/cache/gerboweb/job_queue.sqlite3"

Wyświetl plik

@ -0,0 +1,166 @@
#!/usr/bin/env python3
# TODO setup webserver user disk quota
import tempfile
import uuid
from functools import wraps
from os import path
import os
import sqlite3
from flask import Flask, url_for, redirect, session, make_response, render_template, request, send_file, abort, flash
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileRequired
from wtforms.fields import RadioField
from wtforms.validators import DataRequired
from werkzeug.utils import secure_filename
from job_queue import JobQueue
app = Flask(__name__, static_url_path='/static')
app.config.from_envvar('GERBOWEB_SETTINGS')
class UploadForm(FlaskForm):
upload_file = FileField(validators=[DataRequired()])
class OverlayForm(UploadForm):
upload_file = FileField(validators=[FileRequired()])
side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')],
default=lambda: session.get('side_selected', session.get('last_download')))
class ResetForm(FlaskForm):
pass
job_queue = JobQueue(app.config['JOB_QUEUE_DB'])
def tempfile_path(namespace):
""" Return a path for a per-session temporary file identified by the given namespace. Create the session tempfile
dir if necessary. The application tempfile dir is controlled via the upload_path config value and not managed by
this function. """
if not path.isdir(app.config['UPLOAD_PATH']):
os.mkdir(app.config['UPLOAD_PATH'])
sess_tmp = path.join(app.config['UPLOAD_PATH'], session['session_id'])
if not path.isdir(sess_tmp):
os.mkdir(sess_tmp)
return path.join(sess_tmp, namespace)
def require_session_id(fun):
@wraps(fun)
def wrapper(*args, **kwargs):
if 'session_id' not in session:
session['session_id'] = str(uuid.uuid4())
return fun(*args, **kwargs)
return wrapper
@app.route('/')
@require_session_id
def index():
forms = {
'gerber_form': UploadForm(),
'overlay_form': OverlayForm(),
'reset_form': ResetForm() }
for job_type in ('vector_job', 'render_job'):
if job_type in session:
job = job_queue[session[job_type]]
if job.finished:
if job.result != 0:
flash(f'Error processing gerber files', 'success') # FIXME make this an error, add CSS
del session[job_type]
r = make_response(render_template('index.html',
has_renders = path.isfile(tempfile_path('gerber.zip')),
has_output = path.isfile(tempfile_path('overlay.png')),
**forms))
if 'vector_job' in session or 'render_job' in session:
r.headers.set('refresh', '10')
return r
# NOTES about the gerber and overlay file upload routines
# * The maximum upload size is limited by the MAX_CONTENT_LENGTH config setting.
# * The uploaded files are deleted after a while by systemd tmpfiles.d
# TODO: validate this setting applies *after* gzip transport compression
def vectorize():
if 'vector_job' in session:
job_queue[session['vector_job']].abort()
session['vector_job'] = job_queue.enqueue('vector',
client=request.remote_addr,
session_id=session['session_id'],
side=session['side_selected'])
def render():
if 'render_job' in session:
job_queue[session['render_job']].abort()
session['render_job'] = job_queue.enqueue('render',
session_id=session['session_id'],
client=request.remote_addr)
@app.route('/upload/gerber', methods=['POST'])
@require_session_id
def upload_gerber():
upload_form = UploadForm()
if upload_form.validate_on_submit():
f = upload_form.upload_file.data
f.save(tempfile_path('gerber.zip'))
session['filename'] = secure_filename(f.filename) # Cache filename for later download
render()
if path.isfile(tempfile_path('overlay.png')): # Re-vectorize when gerbers change
vectorize()
flash(f'Gerber file successfully uploaded.', 'success')
return redirect(url_for('index'))
@app.route('/upload/overlay', methods=['POST'])
@require_session_id
def upload_overlay():
upload_form = OverlayForm()
if upload_form.validate_on_submit():
# FIXME raise error when no side selected
f = upload_form.upload_file.data
f.save(tempfile_path('overlay.png'))
session['side_selected'] = upload_form.side.data
vectorize()
flash(f'Overlay file successfully uploaded.', 'success')
return redirect(url_for('index'))
@app.route('/render/preview/<side>')
def render_preview(side):
if not side in ('top', 'bottom'):
return abort(400, 'side must be either "top" or "bottom"')
return send_file(tempfile_path(f'render_{side}.small.png'))
@app.route('/render/download/<side>')
def render_download(side):
if not side in ('top', 'bottom'):
return abort(400, 'side must be either "top" or "bottom"')
session['last_download'] = side
return send_file(tempfile_path(f'render_{side}.png'),
mimetype='image/png',
as_attachment=True,
attachment_filename=f'{path.splitext(session["filename"])[0]}_render_{side}.png')
@app.route('/output/download')
def output_download():
return send_file(tempfile_path('gerber_out.zip'),
mimetype='application/zip',
as_attachment=True,
attachment_filename=f'{path.splitext(session["filename"])[0]}_with_artwork.zip')
@app.route('/session_reset', methods=['POST'])
@require_session_id
def session_reset():
if 'render_job' in session:
job_queue[session['render_job']].abort()
if 'vector_job' in session:
job_queue[session['vector_job']].abort()
session.clear()
flash('Session reset', 'success');
return redirect(url_for('index'))

Wyświetl plik

@ -0,0 +1,40 @@
import signal
import subprocess
import logging
import itertools
from job_queue import JobQueue
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('queue', help='job queue sqlite3 database file')
parser.add_argument('--loglevel', '-l', default='info')
args = parser.parse_args()
numeric_level = getattr(logging, args.loglevel.upper(), None)
if not isinstance(numeric_level, int):
raise ValueError('Invalid log level: %s' % loglevel)
logging.basicConfig(level=numeric_level)
job_queue = JobQueue(args.queue)
signal.signal(signal.SIGALRM, lambda *args: None) # Ignore incoming alarm signals while processing jobs
signal.setitimer(signal.ITIMER_REAL, 0.001, 1)
while signal.sigwait([signal.SIGALRM, signal.SIGINT]) == signal.SIGALRM:
logging.debug('Checking for jobs')
for job in job_queue.job_iter('render'):
logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}')
with job:
job.result = subprocess.call(['sudo', '/usr/local/sbin/gerbolyze_render.sh', job['session_id']])
logging.info(f'Finishied processing {job.type} job {job.id}')
for job in job_queue.job_iter('vector'):
logging.info(f'Processing {job.type} job {job.id} session {job["session_id"]} from {job.client} submitted {job.created}')
with job:
job.result = subprocess.call(['sudo', '/usr/local/sbin/gerbolyze_vector.sh', job['session_id'], job['side']])
logging.info(f'Finishied processing {job.type} job {job.id}')
logging.info('Caught SIGINT. Exiting.')

Wyświetl plik

@ -0,0 +1,77 @@
import json
import sqlite3
class JobQueue:
def __init__(self, dbfile):
self.dbfile = dbfile
self.db = sqlite3.connect(dbfile, check_same_thread=False)
self.db.row_factory = sqlite3.Row
with self.db as conn:
conn.execute('''CREATE TABLE IF NOT EXISTS jobs
(id INTEGER PRIMARY KEY,
type TEXT,
params TEXT,
client TEXT,
result INTEGER DEFAULT NULL,
created DATETIME DEFAULT CURRENT_TIMESTAMP,
consumed DATETIME DEFAULT NULL,
aborted DATETIME DEFAULT NULL,
finished DATETIME DEFAULT NULL);''')
def enqueue(self, task_type:str, client, **params):
""" Enqueue a job of the given type with the given params. Returns the new job ID. """
with self.db as conn:
return conn.execute('INSERT INTO jobs(type, client, params) VALUES (?, ?, ?)',
(task_type, client, json.dumps(params))).lastrowid
def pop(self, task_type):
""" Fetch the next job of the given type. Returns a sqlite3.Row object of the job or None if no jobs of the given
type are queued. """
with self.db as conn:
job = conn.execute('SELECT * FROM jobs WHERE type=? AND consumed IS NULL AND aborted IS NULL ORDER BY created ASC LIMIT 1',
(task_type,)).fetchone()
if job is None:
return None
# Atomically commit to this job
conn.execute('UPDATE jobs SET consumed=datetime("now") WHERE id=?', (job['id'],))
return Job(self.db, job)
def job_iter(self, task_type):
return iter(lambda: self.pop(task_type), None)
def __getitem__(self, key):
""" Return the job with the given ID, or raise a KeyError if the key cannot be found. """
with self.db as conn:
job = conn.execute('SELECT * FROM jobs WHERE id=?', (key,)).fetchone()
if job is None:
raise KeyError(f'Unknown job ID "{key}"')
return Job(self.db, job)
class Job(dict):
def __init__(self, db, row):
super().__init__(json.loads(row['params']))
self._db = db
self._row = row
self.id = row['id']
self.type = row['type']
self.client = row['client']
self.created = row['created']
self.consumed = row['consumed']
self.finished = row['finished']
self.result = row['result']
def __enter__(self):
return self
def __exit__(self, _exc_type, _exc_val, _exc_tb):
with self._db as conn:
conn.execute('UPDATE jobs SET finished=datetime("now"), result=? WHERE id=?', (self.result, self.id,))
def abort(self, job_id):
with self.db as conn:
conn.execute('UPDATE jobs SET aborted=datetime("now") WHERE id=?', (self.id,))

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 324 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 173 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 542 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 30 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.0 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 86 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.9 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.7 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 238 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 9.3 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 536 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 293 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 246 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 167 KiB

Wyświetl plik

@ -0,0 +1,339 @@
:root {
--c-blue1: #19aeff;
--c-blue2: #0084c8;
--c-blue3: #005c94;
--c-red1: #ff4141;
--c-red2: #dc0000;
--c-red3: #b50000;
--c-orange1: #ffff3e;
--c-orange2: #ff9900;
--c-orange3: #ff6600;
--c-brown1: #ffc022;
--c-brown2: #b88100;
--c-brown3: #804d00;
--c-green1: #ccff42;
--c-green2: #9ade00;
--c-green3: #009100;
--c-purple1: #f1caff;
--c-purple2: #d76cff;
--c-purple3: #ba00ff;
--c-metallic1: #bdcdd4;
--c-metallic2: #9eabb0;
--c-metallic3: #364e59;
--c-metallic4: #0e232e;
--c-grey1: #ffffff;
--c-grey2: #cccccc;
--c-grey3: #999999;
--c-grey4: #666666;
--c-grey5: #2d2d2d;
--cg1: #003018;
--cg2: #006130;
--cg3: #00964a;
--cg4: #00d167;
--cg5: #4cffa4;
--cg6: #b7ffda;
--cg7: #e1fff0;
--cr1: #300900;
--cr2: #611200;
--cr3: #961c00;
--cr4: #d12700;
--cr5: #ff6e4c;
--cr6: #ffc5b7;
--cr7: #ffe7e1;
--cb1: #001b30;
--cb2: #003761;
--cb3: #005596;
--cb4: #0076d1;
--cb5: #4cb1ff;
--cb6: #b7e0ff;
--cb7: #e1f2ff;
--cb8: #f5fbff;
}
body {
font-family: 'Helvetica', 'Arial', sans-serif;
color: var(--cb1);
display: flex;
flex-direction: row;
justify-content: center;
margin: 0;
background-color: var(--cb8);
}
.layout-container {
flex-basis: 55em;
flex-shrink: 1;
flex-grow: 0;
padding: 45px;
background-color: white;
}
div.header {
background-image: url("/static/bg10.jpg");
background-position: center;
background-size: cover;
background-repeat: no-repeat;
display: flex;
margin-left: -45px;
margin-right: -45px;
margin-bottom: 3em;
padding-left: 3em;
padding-right: 3em;
text-shadow: 1px 1px 1px black;
}
div.flash-success {
background-color: var(--cg6);
color: var(--cg1);
text-shadow: 0 0 2px var(--cg7);
border-radius: 5px;
margin: 1em;
padding-left: 3em;
padding-right: 3em;
padding-top: 2em;
padding-bottom: 2em;
}
div.flash-success::before {
content: "Success!";
display: block;
font-weight: bold;
font-size: 16pt;
margin-right: 1em;
margin-bottom: 0.5em;
}
div.desc {
margin-top: 5em;
margin-bottom: 7em;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
text-align: justify;
color: white;
}
div.loading-message {
text-align: center;
margin-top: 2em;
}
.steps {
display: flex;
flex-direction: column;
counter-reset: step;
}
.step {
display: flex;
flex-direction: row;
align-items: flex-start;
justify-content: center;
flex-wrap: wrap;
width: 100%;
padding-top: 20px;
position: relative;
margin-bottom: 1em;
margin-top: 2em;
}
.step > .description::before {
counter-increment: step;
content: counter(step);
font-weight: 700;
font-size: 30px;
text-align: center;
border-radius: 50%;
background-color: var(--cg5);
display: block;
position: absolute;
top: 15px;
left: 0;
width: 60px;
line-height: 60px;
}
.step > .description {
flex-basis: 20em;
flex-shrink: 0;
flex-grow: 0;
margin-left: 20px;
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
text-align: justify;
}
.step > .description > h2 {
text-align: right;
margin-top: 0;
padding-left: 60px;
height: 60px;
}
.step > .controls {
flex-grow: 1;
flex-shrink: 1;
display: flex;
flex-direction: column;
align-items: stretch;
margin-right: 1em;
margin-left: 1em;
padding: 1em;
background-color: var(--cb8);
border-radius: 5px;
}
input.reset-button {
background-color: var(--cr4);
color: white;
text-shadow: 0 0 2px var(--cr1);
border: 0;
border-radius: 5px;
padding: 0.5em 1em 0.5em 1em;
}
input.submit-button {
background-color: var(--cg4);
color: var(--cg1);
text-shadow: 0 0 2px var(--cg7);
font-weight: bold;
margin-left: 1em;
border: 0;
border-radius: 5px;
padding: 0.5em 1em 0.5em 1em;
}
.controls > .form-controls {
margin-bottom: 1em;
}
.controls > .submit-buttons {
margin-top: 1em;
text-align: right;
}
.controls > .download-controls {
padding: 1em;
margin-bottom: 1em;
display: flex;
flex-direction: column;
align-items: center;
}
a.output-download:link, a.output-download:hover, a.output-download:visited, a.output-download:active {
font-size: 30pt;
font-weight: bold;
color: var(--cb1);
text-shadow: 0.5px 0.5px 0.5px var(--cb6);
}
.preview-images {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-around;
}
a.preview:link, a.preview:hover, a.preview:visited, a.preview:active {
text-decoration: none;
width: 200px;
height: 200px;
border-radius: 5px;
margin: 1em;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--cb3);
background-blend-mode: multiply;
background-size: contain;
background-repeat: no-repeat;
background-position: 50% 50%;
box-shadow: 1px 1px 5px 1px #001b304d;
}
.overlay {
text-align: center;
font-size: 50pt;
font-weight: bold;
color: var(--cg4);
mix-blend-mode: screen;
}
.sample-images {
text-align: center;
}
.sample-images > h1 {
color: white;
padding-top: 5px;
line-height: 70px;
/* background-image: linear-gradient(to top right, var(--cg5), var(--cg6)); */
background-image: url("/static/bg10.jpg");
background-position: center;
background-size: cover;
background-repeat: no-repeat;
margin-left: -45px;
margin-right: -45px;
margin-top: 3em;
text-shadow: 1px 1px 1px black;
}
.sample-images > img {
width: 300px;
height: 300px;
margin: 1em;
}
/* Spinner from https://loading.io/css/ */
.lds-ring {
display: inline-block;
position: relative;
width: 64px;
height: 64px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 51px;
height: 51px;
margin: 6px;
border: 6px solid var(--cb1);
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: var(--cb1) transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

Wyświetl plik

@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Gerbolyze Raster image to PCB renderer</title>
<link rel="stylesheet" type="text/css" href="{{url_for('static', filename='style.css')}}">
<link rel="icon" type="image/png" href="{{url_for('static', filename='favicon-512.png')}}">
<link rel="apple-touch-icon" href="{{url_for('static', filename='favicon-512.png')}}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<div class="layout-container">
<div class="header">
<div class="desc">
<h1>Raster image to PCB converter</h1>
<p>
Gerbolyze is a tool for rendering black and white raster (PNG) images directly onto gerber layers. You can
use this to put art on a PCB's silkscreen, solder mask or copper layers. The input is a black-and-white PNG
image that is vectorized and rendered into an existing gerber file. Gerbolyze works with gerber files
produced with any EDA toolchain and has been tested to work with both Altium and KiCAD.
</p>
</div>
</div>
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
<div class="flashes">
{% for category, message in messages %}
<div class="flash flash-{{category}}">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
{% endwith %}
<form id="reset-form" method="POST" action="{{url_for('session_reset')}}" class="reset-form">{{reset_form.csrf_token}}</form>
<div class="steps">
<div class="step" id="step1">
<div class="description">
<h2>Upload zipped gerber files</h2>
<p>
First, upload a zip file containing all your gerber files. The default file names used by KiCAD, Eagle
and Altium are supported.
</p>
</div>
<div class="controls">
<form id="gerber-upload-form" method="POST" action="{{url_for('upload_gerber')}}" enctype="multipart/form-data">
{{gerber_form.csrf_token}}
</form>
<div class="form-controls">
<div class="upload-label">Upload Gerber file:</div>
<input class='upload-button' form="gerber-upload-form" name="upload_file" size="20" type="file">
</div>
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
<input class='submit-button' form="gerber-upload-form" type="submit" value="Submit">
</div>
</div>
</div>
{% if 'render_job' in session or has_renders %}
<div class="step" id="step2">
<div class="description">
<h2>Download the target side's preview image</h2>
<p>
Second, download either the top or bottom preview image and use it to align and scale your own artwork
in an image editing program such as Gimp. Then upload your overlay image below.
Note that you will have to convert grayscale images into binary images yourself. Gerbolyze can't do this
for you since there are lots of variables involved. Our <a href="https://github.com/jaseg/gerbolyze/blob/master/README.rst#image-preprocessing-guide">Guideline on image processing</a> gives an overview on
<i>one</i> way to produce agreeable binary images from grayscale source material.
</p>
</div>
<div class="controls">
{% if 'render_job' in session %}
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div>
{% else %}
<div class="preview-images">
<a href="{{url_for('render_download', side='top')}}" onclick="document.querySelector('#side-0').checked=true" class="preview preview-top" style="background-image:url('{{url_for('render_preview', side='top')}}');">
<div class="overlay">top</div>
</a>
<a href="{{url_for('render_download', side='bottom')}}" onclick="document.querySelector('#side-1').checked=true" class="preview preview-bottom" style="background-image:url('{{url_for('render_preview', side='bottom')}}');">
<div class="overlay">bot<br/>tom</div>
</a>
</div>
{% endif %}
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
</div>
</div>
</div>
<div class="step" id="step3">
<div class="description">
<h2>Upload overlay image</h2>
<p>
Now, upload your binary overlay image as a PNG and let gerbolyze render it onto the target layer. The PNG
file should be a black and white binary file with details generally above about 10px size. <b>Antialiased
edges are supported.</b>
</p>
</div>
<div class="controls">
<form id="overlay-upload-form" method="POST" action="{{url_for('upload_overlay')}}" enctype="multipart/form-data">
{{overlay_form.csrf_token}}
</form>
<div class="form-controls">
<div class="form-label upload-label">Upload Overlay PNG file:</div>
<input class='upload-button' form="overlay-upload-form" name="upload_file" size="20" type="file">
</div>
<div class="form-controls">
<div class="form-label target-label">Target layer:</div>
<input form="overlay-upload-form" name="side" id="side-0" type="radio" value="top">
<label for="side-0">Top</label>
<input form="overlay-upload-form" name="side" id="side-1" type="radio" value="top">
<label for="side-1">Bottom</label>
</div>
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
<input class='submit-button' form="overlay-upload-form" type="submit" value="Submit">
</div>
</div>
</div>
{% if 'vector_job' in session or has_output %}
<div class="step" id="step4">
<div class="description">
<h2>Download the processed gerber files</h2>
</div>
<div class="controls">
{% if 'vector_job' in session %}
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div>
{% else %}
<div class='download-controls'>
<a class='output-download' href="{{url_for('output_download')}}">Click to download</a>
</div>
{% endif %}
<div class="submit-buttons">
<input class='reset-button' form="reset-form" type="submit" value="Start over">
</div>
<!--4>Debug foo</h4>
<div class="loading-message">
<div class="lds-ring"><div></div><div></div><div></div><div></div></div>
<div><strong>Processing...</strong></div>
<div>(this may take several minutes!)</div>
</div-->
</div>
</div>
{% endif %} {# vector job #}
{% endif %} {# render job #}
</div>
<div class="sample-images">
<h1>Sample images</h1>
<img src="{{url_for('static', filename='sample1.jpg')}}">
<img src="{{url_for('static', filename='sample2.jpg')}}">
<img src="{{url_for('static', filename='sample3.jpg')}}">
</div>
</div>
</body>
</html>

BIN
sample1.jpg 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 293 KiB

BIN
sample2.jpg 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 246 KiB

BIN
sample3.jpg 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 167 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 110 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 362 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 149 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 380 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 365 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 307 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 39 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 280 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 254 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 310 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.1 MiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 163 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 568 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 503 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 334 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 114 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 60 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 39 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 24 KiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 25 KiB

36
setup.py 100755
Wyświetl plik

@ -0,0 +1,36 @@
#!/usr/bin/env python3
from setuptools import setup
def readme():
with open('README.rst') as f:
return f.read()
setup(
name = 'gerbolyze',
version = '0.1.10',
py_modules = ['gerbolyze'],
scripts = ['gerbolyze'],
description = ('A high-resolution image-to-PCB converter. Gerbolyze reads and vectorizes black-and-white raster '
'images, then plots the vectorized image into an existing gerber file while avoiding existing features such as '
'text or holes.'),
long_description=readme(),
long_description_content_type='text/x-rst',
url = 'https://github.com/jaseg/gerbolyze',
author = 'jaseg',
author_email = 'github@jaseg.net',
install_requires = ['pcb-tools', 'tqdm', 'numpy', 'opencv-python'],
license = 'AGPLv3',
classifiers = [
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Intended Audience :: Manufacturing',
'Intended Audience :: Science/Research',
'Intended Audience :: Religion',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)',
'Natural Language :: English',
'Topic :: Scientific/Engineering :: Electronic Design Automation (EDA)',
'Topic :: Utilities'
]
)

Wyświetl plik

@ -0,0 +1,22 @@
G04 #@! TF.FileFunction,Profile,NP*
%FSLAX46Y46*%
G04 Gerber Fmt 4.6, Leading zero omitted, Abs format (unit mm)*
G04 Created by KiCad (PCBNEW 4.0.6) date Mon Sep 18 11:29:53 2017*
%MOMM*%
%LPD*%
G01*
G04 APERTURE LIST*
%ADD10C,0.100000*%
%ADD11C,0.150000*%
G04 APERTURE END LIST*
D10*
D11*
X149000000Y-43750000D02*
X49000000Y-43750000D01*
X149000000Y-121750000D02*
X149000000Y-43750000D01*
X49000000Y-121750000D02*
X149000000Y-121750000D01*
X49000000Y-43750000D02*
X49000000Y-121750000D01*
M02*

22840
testdata/chibi_2024-F.SilkS.gbr vendored 100644

Plik diff jest za duży Load Diff

33
testdata/led_drv.GKO vendored 100644
Wyświetl plik

@ -0,0 +1,33 @@
G04 Layer_Color=16711935*
%FSLAX25Y25*%
%MOIN*%
G70*
G01*
G75*
%ADD26C,0.01000*%
D26*
X354331Y177165D02*
G03*
X334646Y196850I-19685J0D01*
G01*
Y0D02*
G03*
X354331Y19685I0J19685D01*
G01*
X0D02*
G03*
X19685Y0I19685J0D01*
G01*
Y196850D02*
G03*
X0Y177165I0J-19685D01*
G01*
X354331Y19685D02*
Y177165D01*
X19685Y196850D02*
X334646D01*
X19685Y0D02*
X334646D01*
X0Y19685D02*
Y177165D01*
M02*

5494
testdata/led_drv.GTO vendored 100644

Plik diff jest za duży Load Diff

372
testdata/led_drv.GTP vendored 100644
Wyświetl plik

@ -0,0 +1,372 @@
G04 Layer_Color=8421504*
%FSLAX25Y25*%
%MOIN*%
G70*
G01*
G75*
G04:AMPARAMS|DCode=10|XSize=25.59mil|YSize=64.96mil|CornerRadius=1.92mil|HoleSize=0mil|Usage=FLASHONLY|Rotation=0.000|XOffset=0mil|YOffset=0mil|HoleType=Round|Shape=RoundedRectangle|*
%AMROUNDEDRECTD10*
21,1,0.02559,0.06112,0,0,0.0*
21,1,0.02175,0.06496,0,0,0.0*
1,1,0.00384,0.01088,-0.03056*
1,1,0.00384,-0.01088,-0.03056*
1,1,0.00384,-0.01088,0.03056*
1,1,0.00384,0.01088,0.03056*
%
%ADD10ROUNDEDRECTD10*%
%ADD11R,0.03543X0.02756*%
%ADD12R,0.07480X0.04331*%
%ADD13R,0.02756X0.03543*%
%ADD14O,0.05512X0.01772*%
%ADD15R,0.05512X0.05906*%
%ADD16O,0.02756X0.09843*%
%ADD17O,0.01378X0.05906*%
%ADD18R,0.03937X0.03937*%
%ADD19R,0.12992X0.10630*%
%ADD20R,0.03937X0.10630*%
%ADD21R,0.05906X0.05512*%
D10*
X73051Y110925D02*
D03*
X78051D02*
D03*
X83051D02*
D03*
X88051D02*
D03*
X93051D02*
D03*
X98051D02*
D03*
X103051D02*
D03*
X108051D02*
D03*
X73051Y133169D02*
D03*
X78051D02*
D03*
X83051D02*
D03*
X88051D02*
D03*
X93051D02*
D03*
X98051D02*
D03*
X103051D02*
D03*
X108051D02*
D03*
X206909Y106988D02*
D03*
X211910D02*
D03*
X216910D02*
D03*
X221909D02*
D03*
X226909D02*
D03*
X231909D02*
D03*
X236910D02*
D03*
X241910D02*
D03*
X206909Y129232D02*
D03*
X211910D02*
D03*
X216910D02*
D03*
X221909D02*
D03*
X226909D02*
D03*
X231909D02*
D03*
X236910D02*
D03*
X241910D02*
D03*
Y85925D02*
D03*
X236910D02*
D03*
X231909D02*
D03*
X226909D02*
D03*
X221909D02*
D03*
X216910D02*
D03*
X211910D02*
D03*
X206909D02*
D03*
X241910Y63681D02*
D03*
X236910D02*
D03*
X231909D02*
D03*
X226909D02*
D03*
X221909D02*
D03*
X216910D02*
D03*
X211910D02*
D03*
X206909D02*
D03*
X108051Y85925D02*
D03*
X103051D02*
D03*
X98051D02*
D03*
X93051D02*
D03*
X88051D02*
D03*
X83051D02*
D03*
X78051D02*
D03*
X73051D02*
D03*
X108051Y63681D02*
D03*
X103051D02*
D03*
X98051D02*
D03*
X93051D02*
D03*
X88051D02*
D03*
X83051D02*
D03*
X78051D02*
D03*
X73051D02*
D03*
D11*
X116142Y132087D02*
D03*
Y126181D02*
D03*
X118110Y113189D02*
D03*
Y119095D02*
D03*
X270079Y129331D02*
D03*
Y135236D02*
D03*
X251575Y117913D02*
D03*
Y112008D02*
D03*
X264567Y130118D02*
D03*
Y124213D02*
D03*
X270079Y123819D02*
D03*
Y117913D02*
D03*
Y112402D02*
D03*
Y106496D02*
D03*
X264567Y112402D02*
D03*
Y106496D02*
D03*
X302362Y111221D02*
D03*
Y117126D02*
D03*
X198031Y72638D02*
D03*
Y78543D02*
D03*
X175591Y97441D02*
D03*
Y103347D02*
D03*
X182677Y97441D02*
D03*
Y91535D02*
D03*
X114567Y108071D02*
D03*
Y102165D02*
D03*
X107087Y102953D02*
D03*
Y97047D02*
D03*
X101575Y102953D02*
D03*
Y97047D02*
D03*
X63386Y76968D02*
D03*
Y82874D02*
D03*
X7874Y57087D02*
D03*
Y62992D02*
D03*
X11811Y132480D02*
D03*
Y138386D02*
D03*
D12*
X140748Y113189D02*
D03*
X126181D02*
D03*
Y122244D02*
D03*
X140748D02*
D03*
D13*
X130512Y129528D02*
D03*
X136417D02*
D03*
X288779Y88976D02*
D03*
X282874D02*
D03*
X288386Y106299D02*
D03*
X282480D02*
D03*
X128937Y72047D02*
D03*
X134843D02*
D03*
X14764Y51181D02*
D03*
X8858D02*
D03*
D14*
X293504Y112303D02*
D03*
Y114862D02*
D03*
Y117421D02*
D03*
Y119980D02*
D03*
X277362Y112303D02*
D03*
Y114862D02*
D03*
Y117421D02*
D03*
Y119980D02*
D03*
D15*
X281890Y98425D02*
D03*
X289764D02*
D03*
X15748Y70866D02*
D03*
X23622D02*
D03*
X15748Y60236D02*
D03*
X23622D02*
D03*
D16*
X166949Y110039D02*
D03*
X161949D02*
D03*
X156949D02*
D03*
X151949D02*
D03*
X166949Y90748D02*
D03*
X161949D02*
D03*
X156949D02*
D03*
X151949D02*
D03*
D17*
X120374Y79921D02*
D03*
X122933D02*
D03*
X125492D02*
D03*
X128051D02*
D03*
X130610D02*
D03*
X133169D02*
D03*
X135728D02*
D03*
X138287D02*
D03*
X140847D02*
D03*
X143405D02*
D03*
X120374Y101969D02*
D03*
X122933D02*
D03*
X125492D02*
D03*
X128051D02*
D03*
X130610D02*
D03*
X133169D02*
D03*
X135728D02*
D03*
X138287D02*
D03*
X140847D02*
D03*
X143405D02*
D03*
D18*
X7874Y42717D02*
D03*
Y36024D02*
D03*
X15748Y42717D02*
D03*
Y36024D02*
D03*
D19*
X19685Y110039D02*
D03*
D20*
X28740Y86811D02*
D03*
X19685D02*
D03*
X10630D02*
D03*
D21*
X43307Y94882D02*
D03*
Y102756D02*
D03*
M02*

163
testdata/led_drv.TXT vendored 100644
Wyświetl plik

@ -0,0 +1,163 @@
M48
;Layer_Color=9474304
;FILE_FORMAT=2:5
INCH,LZ
;TYPE=PLATED
T1F00S00C0.01200
T2F00S00C0.01600
T3F00S00C0.02400
T4F00S00C0.03150
T5F00S00C0.03937
T6F00S00C0.04291
T7F00S00C0.04724
T8F00S00C0.06299
T9F00S00C0.06693
T10F00S00C0.12835
;TYPE=NON_PLATED
T11F00S00C0.12598
%
T01
X0054724Y0049606
Y0055905
X0062598Y0070472
X007559Y0070866
X0086614Y0069291
X0088189Y0057874
X01Y0070472
X0104464Y0070079
X0105118Y007874
X0122441Y0087795
X0129921Y006496
X0132433Y0067085
X0135433Y0066929
X0138582Y006811
X0138189Y0063386
X0136221Y006063
X0117323Y0058268
X0116535Y0048425
X0120866Y0049213
X0146063Y0075197
X0146457Y0078347
X0080709Y0080709
X0168504Y0072835
X0169685Y0069291
X0193701Y007126
X019685Y0067716
X0201181Y006811
X0214173Y0069685
X0212598Y0072835
X0236221Y007126
X0248819Y0064173
X0251575Y0052756
X0227559Y0056299
X0194881Y0084252
X0199606Y0122047
X0200394Y0129921
X0216929Y0123228
X022559Y0135827
X0244488Y0136221
X0245669Y0132283
X0255906Y0129921
X023937Y0113386
X0294357Y0106299
X0144488Y013937
X01209Y0149179
X0114173Y0135827
X011063Y0124409
X0106299Y0116141
X01Y0127559
X0089764Y0138583
X0079921Y012559
X0069291Y0127165
X0059449Y0129134
T02
X010748Y005315
X0116535Y0077559
T03
X0146063Y0062598
X0139085Y0071763
X0139764Y0137795
X0126378Y0146063
T04
X003937Y0064961
Y0084646
X0177165
Y0064961
Y0112205
Y013189
X003937
Y0112205
T05
X0157874Y0014134
Y0024134
Y0034134
Y0044134
X0007087Y0145
Y0155
T06
X0072205Y0033779
X008311
X0094016
X0104921
X0110374Y0044961
X0099468
X0088563
X0077658
X0066752
X0204567Y0045276
X0215472
X0226378
X0237283
X0248189
X0242736Y0034095
X0231831
X0220925
X021002
X0215472Y0153858
X0204567
X021002Y0165039
X0220925
X0231831
X0242736
X0248189Y0153858
X0237283
X0226378
X0110394
X0099488
X0088583
X0077677
X0066772
X0072224Y0165039
X008313
X0094035
X0104941
T07
X0334646Y0068425
Y0088425
Y0108425
Y0128425
T08
X0285433Y0062992
X0305118
Y0133858
X0285433
T09
X0267717Y0098425
X0307087
T10
X003937Y003937
X0137756
X0177185Y0039685
X0275571
X0275571Y0159449
X0177185
X0137776
X003939
T11
X0011811Y0019685
X015748Y0062992
X0326772Y0019685
Y0177165
X015748Y0133858
X0011811Y0177165
M30

162
testdata/test.svg vendored 100644
Wyświetl plik

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="70mm"
height="50mm"
viewBox="0 0 70 50"
version="1.1"
id="svg8"
inkscape:version="0.92.2 (5c3e80d, 2017-08-06)"
sodipodi:docname="test.svg"
inkscape:export-filename="/home/user/toys/gerbimg/test.svg.png"
inkscape:export-xdpi="290.29001"
inkscape:export-ydpi="290.29001">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="1"
inkscape:pageshadow="2"
inkscape:zoom="2.8284271"
inkscape:cx="139.16551"
inkscape:cy="85.907173"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1918"
inkscape:window-height="1026"
inkscape:window-x="1617"
inkscape:window-y="154"
inkscape:window-maximized="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-247)">
<rect
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375005"
id="rect4504"
width="10"
height="10"
x="0"
y="247" />
<rect
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375005"
id="rect4504-3"
width="10"
height="10"
x="60"
y="247" />
<rect
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375005"
id="rect4504-6"
width="10"
height="10"
x="60"
y="287" />
<rect
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375005"
id="rect4504-7"
width="10"
height="10"
x="0"
y="287" />
<rect
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375005"
id="rect4504-5"
width="10"
height="10"
x="60"
y="267" />
<rect
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375005"
id="rect4504-35"
width="10"
height="10"
x="0"
y="267" />
<rect
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375005"
id="rect4504-62"
width="10"
height="10"
x="30"
y="247" />
<rect
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79375005"
id="rect4504-9"
width="10"
height="10"
x="30"
y="287" />
<circle
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79374999"
id="path4563"
cx="35"
cy="272"
r="7.5000005" />
<circle
style="opacity:0.98000004;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.79374999"
id="path4563-1"
cx="35"
cy="272"
r="5" />
<circle
style="opacity:0.98000004;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.79374999"
id="path4563-2"
cx="35"
cy="272"
r="2.5" />
<flowRoot
xml:space="preserve"
id="flowRoot4586"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:15px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.69816089px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
transform="matrix(0.37897186,0,0,0.37897186,-10.192843,241.84261)"><flowRegion
id="flowRegion4588"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';stroke-width:0.69816089px"><rect
id="rect4590"
width="101.46983"
height="72.832001"
x="55.507881"
y="18.917198"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';stroke-width:0.69816089px" /></flowRegion><flowPara
id="flowPara4592"
style="stroke-width:0.69816089px">↑UP↑</flowPara></flowRoot> <flowRoot
xml:space="preserve"
id="flowRoot4586-7"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:15px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.69816089px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
transform="matrix(0.37897186,0,0,0.37897186,19.807155,241.84261)"><flowRegion
id="flowRegion4588-0"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';stroke-width:0.69816089px"><rect
id="rect4590-9"
width="101.46983"
height="72.832001"
x="55.507881"
y="18.917198"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:sans-serif;-inkscape-font-specification:'sans-serif Bold';stroke-width:0.69816089px" /></flowRegion><flowPara
id="flowPara4592-3"
style="stroke-width:0.69816089px">↑UP↑</flowPara></flowRoot> </g>
</svg>

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 6.1 KiB

BIN
testdata/test.svg.png vendored 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 16 KiB