aboutsummaryrefslogtreecommitdiff
path: root/gerboweb
diff options
context:
space:
mode:
Diffstat (limited to 'gerboweb')
-rw-r--r--gerboweb/colors.svg357
-rw-r--r--gerboweb/gerboweb.cfg4
-rw-r--r--gerboweb/gerboweb.py166
-rw-r--r--gerboweb/job_processor.py40
-rw-r--r--gerboweb/job_queue.py77
-rw-r--r--gerboweb/static/bg.jpgbin0 -> 331765 bytes
-rw-r--r--gerboweb/static/bg10.jpgbin0 -> 177533 bytes
-rw-r--r--gerboweb/static/favicon-1024.pngbin0 -> 555502 bytes
-rw-r--r--gerboweb/static/favicon-128.pngbin0 -> 30221 bytes
-rw-r--r--gerboweb/static/favicon-16.pngbin0 -> 1040 bytes
-rw-r--r--gerboweb/static/favicon-256.pngbin0 -> 87765 bytes
-rw-r--r--gerboweb/static/favicon-32.pngbin0 -> 2945 bytes
-rw-r--r--gerboweb/static/favicon-48.pngbin0 -> 5850 bytes
-rw-r--r--gerboweb/static/favicon-512.pngbin0 -> 243219 bytes
-rw-r--r--gerboweb/static/favicon-64.pngbin0 -> 9566 bytes
-rw-r--r--gerboweb/static/favicon.pngbin0 -> 549230 bytes
-rw-r--r--gerboweb/static/sample1.jpgbin0 -> 299841 bytes
-rw-r--r--gerboweb/static/sample2.jpgbin0 -> 251440 bytes
-rw-r--r--gerboweb/static/sample3.jpgbin0 -> 171160 bytes
-rw-r--r--gerboweb/static/style.css339
-rw-r--r--gerboweb/templates/index.html167
21 files changed, 1150 insertions, 0 deletions
diff --git a/gerboweb/colors.svg b/gerboweb/colors.svg
new file mode 100644
index 0000000..d7b2eee
--- /dev/null
+++ b/gerboweb/colors.svg
@@ -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>
diff --git a/gerboweb/gerboweb.cfg b/gerboweb/gerboweb.cfg
new file mode 100644
index 0000000..02ea211
--- /dev/null
+++ b/gerboweb/gerboweb.cfg
@@ -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"
diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py
new file mode 100644
index 0000000..2633e7b
--- /dev/null
+++ b/gerboweb/gerboweb.py
@@ -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'))
+
diff --git a/gerboweb/job_processor.py b/gerboweb/job_processor.py
new file mode 100644
index 0000000..c138bf4
--- /dev/null
+++ b/gerboweb/job_processor.py
@@ -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.')
+
diff --git a/gerboweb/job_queue.py b/gerboweb/job_queue.py
new file mode 100644
index 0000000..62ba398
--- /dev/null
+++ b/gerboweb/job_queue.py
@@ -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,))
+
diff --git a/gerboweb/static/bg.jpg b/gerboweb/static/bg.jpg
new file mode 100644
index 0000000..94856fc
--- /dev/null
+++ b/gerboweb/static/bg.jpg
Binary files differ
diff --git a/gerboweb/static/bg10.jpg b/gerboweb/static/bg10.jpg
new file mode 100644
index 0000000..9d14fd3
--- /dev/null
+++ b/gerboweb/static/bg10.jpg
Binary files differ
diff --git a/gerboweb/static/favicon-1024.png b/gerboweb/static/favicon-1024.png
new file mode 100644
index 0000000..ed33689
--- /dev/null
+++ b/gerboweb/static/favicon-1024.png
Binary files differ
diff --git a/gerboweb/static/favicon-128.png b/gerboweb/static/favicon-128.png
new file mode 100644
index 0000000..8bdefac
--- /dev/null
+++ b/gerboweb/static/favicon-128.png
Binary files differ
diff --git a/gerboweb/static/favicon-16.png b/gerboweb/static/favicon-16.png
new file mode 100644
index 0000000..4164370
--- /dev/null
+++ b/gerboweb/static/favicon-16.png
Binary files differ
diff --git a/gerboweb/static/favicon-256.png b/gerboweb/static/favicon-256.png
new file mode 100644
index 0000000..364a3bb
--- /dev/null
+++ b/gerboweb/static/favicon-256.png
Binary files differ
diff --git a/gerboweb/static/favicon-32.png b/gerboweb/static/favicon-32.png
new file mode 100644
index 0000000..f46cf2b
--- /dev/null
+++ b/gerboweb/static/favicon-32.png
Binary files differ
diff --git a/gerboweb/static/favicon-48.png b/gerboweb/static/favicon-48.png
new file mode 100644
index 0000000..c9a8c19
--- /dev/null
+++ b/gerboweb/static/favicon-48.png
Binary files differ
diff --git a/gerboweb/static/favicon-512.png b/gerboweb/static/favicon-512.png
new file mode 100644
index 0000000..10b2234
--- /dev/null
+++ b/gerboweb/static/favicon-512.png
Binary files differ
diff --git a/gerboweb/static/favicon-64.png b/gerboweb/static/favicon-64.png
new file mode 100644
index 0000000..76279e2
--- /dev/null
+++ b/gerboweb/static/favicon-64.png
Binary files differ
diff --git a/gerboweb/static/favicon.png b/gerboweb/static/favicon.png
new file mode 100644
index 0000000..a22a3a7
--- /dev/null
+++ b/gerboweb/static/favicon.png
Binary files differ
diff --git a/gerboweb/static/sample1.jpg b/gerboweb/static/sample1.jpg
new file mode 100644
index 0000000..948da6f
--- /dev/null
+++ b/gerboweb/static/sample1.jpg
Binary files differ
diff --git a/gerboweb/static/sample2.jpg b/gerboweb/static/sample2.jpg
new file mode 100644
index 0000000..ef47bd4
--- /dev/null
+++ b/gerboweb/static/sample2.jpg
Binary files differ
diff --git a/gerboweb/static/sample3.jpg b/gerboweb/static/sample3.jpg
new file mode 100644
index 0000000..780c080
--- /dev/null
+++ b/gerboweb/static/sample3.jpg
Binary files differ
diff --git a/gerboweb/static/style.css b/gerboweb/static/style.css
new file mode 100644
index 0000000..eb926dc
--- /dev/null
+++ b/gerboweb/static/style.css
@@ -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);
+ }
+}
+
diff --git a/gerboweb/templates/index.html b/gerboweb/templates/index.html
new file mode 100644
index 0000000..a19fc88
--- /dev/null
+++ b/gerboweb/templates/index.html
@@ -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>