Merge old gerbolyze history
|
@ -1 +1,3 @@
|
|||
build
|
||||
dist
|
||||
gerbolyze.egg-info
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
include README.rst
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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 |
|
@ -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"
|
|
@ -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'))
|
||||
|
|
@ -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.')
|
||||
|
|
@ -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,))
|
||||
|
Po Szerokość: | Wysokość: | Rozmiar: 324 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 173 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 542 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 30 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.0 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 86 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 2.9 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 5.7 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 238 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 9.3 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 536 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 293 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 246 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 167 KiB |
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
Po Szerokość: | Wysokość: | Rozmiar: 293 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 246 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 167 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 110 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 362 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 149 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 380 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 365 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 307 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 39 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 280 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 254 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 310 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 1.1 MiB |
Po Szerokość: | Wysokość: | Rozmiar: 163 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 568 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 503 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 334 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 114 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 60 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 39 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 24 KiB |
Po Szerokość: | Wysokość: | Rozmiar: 25 KiB |
|
@ -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'
|
||||
]
|
||||
)
|
|
@ -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*
|
|
@ -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*
|
|
@ -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*
|
|
@ -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
|
|
@ -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 |
Po Szerokość: | Wysokość: | Rozmiar: 16 KiB |