diff options
author | jaseg <git@jaseg.de> | 2021-01-30 20:19:34 +0100 |
---|---|---|
committer | jaseg <git@jaseg.de> | 2021-01-30 20:19:34 +0100 |
commit | c5f8416b63139ec5b69c318e1be70ae54755e1e6 (patch) | |
tree | d1d683996f0d3bf4cb65bce44e4e03254aadb3a0 /gerboweb | |
parent | 2133867c8a86337c6668f9cfff06e4de9bd0bcce (diff) | |
parent | 3f3b8487d41b3e0beb081ff049e2c815d7e6b269 (diff) | |
download | gerbolyze-c5f8416b63139ec5b69c318e1be70ae54755e1e6.tar.gz gerbolyze-c5f8416b63139ec5b69c318e1be70ae54755e1e6.tar.bz2 gerbolyze-c5f8416b63139ec5b69c318e1be70ae54755e1e6.zip |
Merge old gerbolyze history
Diffstat (limited to 'gerboweb')
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 Binary files differnew file mode 100644 index 0000000..94856fc --- /dev/null +++ b/gerboweb/static/bg.jpg diff --git a/gerboweb/static/bg10.jpg b/gerboweb/static/bg10.jpg Binary files differnew file mode 100644 index 0000000..9d14fd3 --- /dev/null +++ b/gerboweb/static/bg10.jpg diff --git a/gerboweb/static/favicon-1024.png b/gerboweb/static/favicon-1024.png Binary files differnew file mode 100644 index 0000000..ed33689 --- /dev/null +++ b/gerboweb/static/favicon-1024.png diff --git a/gerboweb/static/favicon-128.png b/gerboweb/static/favicon-128.png Binary files differnew file mode 100644 index 0000000..8bdefac --- /dev/null +++ b/gerboweb/static/favicon-128.png diff --git a/gerboweb/static/favicon-16.png b/gerboweb/static/favicon-16.png Binary files differnew file mode 100644 index 0000000..4164370 --- /dev/null +++ b/gerboweb/static/favicon-16.png diff --git a/gerboweb/static/favicon-256.png b/gerboweb/static/favicon-256.png Binary files differnew file mode 100644 index 0000000..364a3bb --- /dev/null +++ b/gerboweb/static/favicon-256.png diff --git a/gerboweb/static/favicon-32.png b/gerboweb/static/favicon-32.png Binary files differnew file mode 100644 index 0000000..f46cf2b --- /dev/null +++ b/gerboweb/static/favicon-32.png diff --git a/gerboweb/static/favicon-48.png b/gerboweb/static/favicon-48.png Binary files differnew file mode 100644 index 0000000..c9a8c19 --- /dev/null +++ b/gerboweb/static/favicon-48.png diff --git a/gerboweb/static/favicon-512.png b/gerboweb/static/favicon-512.png Binary files differnew file mode 100644 index 0000000..10b2234 --- /dev/null +++ b/gerboweb/static/favicon-512.png diff --git a/gerboweb/static/favicon-64.png b/gerboweb/static/favicon-64.png Binary files differnew file mode 100644 index 0000000..76279e2 --- /dev/null +++ b/gerboweb/static/favicon-64.png diff --git a/gerboweb/static/favicon.png b/gerboweb/static/favicon.png Binary files differnew file mode 100644 index 0000000..a22a3a7 --- /dev/null +++ b/gerboweb/static/favicon.png diff --git a/gerboweb/static/sample1.jpg b/gerboweb/static/sample1.jpg Binary files differnew file mode 100644 index 0000000..948da6f --- /dev/null +++ b/gerboweb/static/sample1.jpg diff --git a/gerboweb/static/sample2.jpg b/gerboweb/static/sample2.jpg Binary files differnew file mode 100644 index 0000000..ef47bd4 --- /dev/null +++ b/gerboweb/static/sample2.jpg diff --git a/gerboweb/static/sample3.jpg b/gerboweb/static/sample3.jpg Binary files differnew file mode 100644 index 0000000..780c080 --- /dev/null +++ b/gerboweb/static/sample3.jpg 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> |