aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorjaseg <code@jaseg.net>2019-03-29 22:09:16 +0900
committerjaseg <git@jaseg.net>2019-03-29 22:09:16 +0900
commit23d392c2f7a744d38a369f8bc91cfd0f215f82b0 (patch)
tree876f3aeebd7f1d1f3bfd0612bba905853ca5d559
parent84148e368d3804c841d566cd0a8f28263445ef97 (diff)
downloadgerbolyze-23d392c2f7a744d38a369f8bc91cfd0f215f82b0.tar.gz
gerbolyze-23d392c2f7a744d38a369f8bc91cfd0f215f82b0.tar.bz2
gerbolyze-23d392c2f7a744d38a369f8bc91cfd0f215f82b0.zip
Working on the design
-rw-r--r--gerboweb/deploy/nginx.conf2
-rw-r--r--gerboweb/deploy/playbook.yml6
-rw-r--r--gerboweb/deploy/uwsgi-gerboweb.ini1
-rw-r--r--gerboweb/gerboweb.py10
-rw-r--r--gerboweb/static/style.css241
-rw-r--r--gerboweb/templates/index.html217
6 files changed, 388 insertions, 89 deletions
diff --git a/gerboweb/deploy/nginx.conf b/gerboweb/deploy/nginx.conf
index c76a3db..6344904 100644
--- a/gerboweb/deploy/nginx.conf
+++ b/gerboweb/deploy/nginx.conf
@@ -65,7 +65,7 @@ http {
include /etc/nginx/default.d/*.conf;
location ^~ /static/ {
- root /var/lib/gerboweb/static;
+ root /var/lib/gerboweb;
}
location / {
diff --git a/gerboweb/deploy/playbook.yml b/gerboweb/deploy/playbook.yml
index 3789c21..9753df6 100644
--- a/gerboweb/deploy/playbook.yml
+++ b/gerboweb/deploy/playbook.yml
@@ -152,12 +152,6 @@
name: uwsgi-app@gerboweb.socket
enabled: yes
- - name: Enable and launch uwsgi systemd service
- systemd:
- name: uwsgi-app@gerboweb.service
- enabled: yes
- state: restarted
-
- name: Enable and launch job processor
systemd:
name: gerboweb-job-processor.service
diff --git a/gerboweb/deploy/uwsgi-gerboweb.ini b/gerboweb/deploy/uwsgi-gerboweb.ini
index 748af71..3c8addd 100644
--- a/gerboweb/deploy/uwsgi-gerboweb.ini
+++ b/gerboweb/deploy/uwsgi-gerboweb.ini
@@ -1,5 +1,4 @@
[uwsgi]
-chmod-socket = 660
master = True
cheap = True
idle = 600
diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py
index 17e03e2..1f8d884 100644
--- a/gerboweb/gerboweb.py
+++ b/gerboweb/gerboweb.py
@@ -9,7 +9,7 @@ from os import path
import os
import sqlite3
-from flask import Flask, url_for, redirect, session, make_response, render_template, request, send_file, abort
+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
@@ -26,7 +26,7 @@ class UploadForm(FlaskForm):
class OverlayForm(UploadForm):
upload_file = FileField(validators=[FileRequired()])
- side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')])
+ side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')], default=lambda: session.get('last_download'))
class ResetForm(FlaskForm):
pass
@@ -56,6 +56,7 @@ def require_session_id(fun):
@app.route('/')
@require_session_id
def index():
+ flash(f'Gerber file successfully uploaded.', 'success')
forms = {
'gerber_form': UploadForm(),
'overlay_form': OverlayForm(),
@@ -108,6 +109,7 @@ def upload(namespace):
session_id=session['session_id'],
side=upload_form.side.data)
+ flash(f'{"Gerber" if namespace == "gerber" else "Overlay"} file successfully uploaded.', 'success')
return redirect(url_for('index'))
@app.route('/render/preview/<side>')
@@ -120,10 +122,12 @@ def render_preview(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.png')
+ attachment_filename=f'{path.splitext(session["filename"])[0]}_render_{side}.png')
@app.route('/output/download')
def output_download():
diff --git a/gerboweb/static/style.css b/gerboweb/static/style.css
new file mode 100644
index 0000000..975c7f2
--- /dev/null
+++ b/gerboweb/static/style.css
@@ -0,0 +1,241 @@
+
+: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;
+}
+
+body {
+ font-family: 'Helvetica', 'Arial', sans-serif;
+ color: var(--c-metallic4);
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ margin: 0;
+ background-color: hsl(10 10% 97%);
+}
+
+.layout-container {
+ flex-basis: 55em;
+ flex-shrink: 1;
+ flex-grow: 0;
+ padding: 3em;
+ background-color: white;
+}
+
+div.flash-success {
+ background-color: var(--c-green1);
+ color: hsl(80 20% 20%);
+ text-shadow: 0 0 2px var(--c-green1);
+ 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;
+}
+
+div.loading-message {
+ text-align: center;
+ margin-top: 2em;
+}
+
+.steps {
+ display: flex;
+ flex-direction: column;
+}
+
+.step {
+ display: flex;
+ flex-direction: row;
+ align-items: flex-start;
+ justify-content: center;
+ flex-wrap: wrap;
+ width: 100%;
+ padding-top: 20px;
+}
+
+.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
+}
+
+.step > .controls {
+ flex-grow: 1;
+ flex-shrink: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ margin-right: 20px;
+ margin-left: 20px;
+
+ padding: 1em;
+
+ background-color: hsl(210 40% 97%);
+ border-radius: 5px;
+}
+
+input.reset-button {
+ background-color: var(--c-red1);
+ color: var(--c-grey1);
+ text-shadow: 0 0 2px var(--c-red3);
+ border: 0;
+ border-radius: 5px;
+ padding: 0.5em 1em 0.5em 1em;
+}
+
+input.submit-button {
+ background-color: var(--c-green2);
+ color: hsl(80 20% 20%);
+ text-shadow: 0 0 2px var(--c-green1);
+ 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(--c-metallic4);
+ text-shadow: 0.5px 0.5px 0.5px var(--c-metallic2);
+}
+
+.preview-images {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: space-around;
+}
+
+.preview {
+ width: 200px;
+ height: 200px;
+ border-radius: 5px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+a.overlay:link, a.overlay:hover, a.overlay:visited, a.overlay:active {
+ text-align: center;
+ font-size: 30pt;
+ font-weight: bold;
+ color: var(--c-metallic4);
+ text-shadow: 0.5px 0.5px 0.5px var(--c-metallic2);
+}
+
+/* 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(--c-metallic4);
+ border-radius: 50%;
+ animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+ border-color: var(--c-metallic4) 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
index c5c8503..3e4c255 100644
--- a/gerboweb/templates/index.html
+++ b/gerboweb/templates/index.html
@@ -1,86 +1,147 @@
<!DOCTYPE html>
-<html>
- <head>
- <title>Gerbolyze Raster image to PCB renderer</title>
- </head>
- <body>
- <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>
+<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')}}">
+ </head>
+ <body>
+ <div class="layout-container">
+ <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 class="step" id="step1">
- <h2>Step 1: 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>
+ {% 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 method="POST" action="{{url_for('upload', namespace='gerber')}}" enctype="multipart/form-data">
- {{gerber_form.csrf_token}}
- {{gerber_form.upload_file.label}} {{gerber_form.upload_file(size=20)}}
- <input type="submit" value="Submit">
- </form>
- </div>
+ <form id="reset-form" method="POST" action="{{url_for('session_reset')}}" class="reset-form">{{reset_form.csrf_token}}</form>
- {% if 'render_job' in session or has_renders %}
- <div class="step" id="step2">
- <h2>Step 2: 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.
+ <div class="steps">
+ <div class="step" id="step1">
+ <div class="description">
+ <h2>Step 1: 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>
- 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="{{url_for('static',
- filename='image_processing_guide.html')}}">Guideline on image processing</a> gives an overview on
- <i>one</i> way to produce agreeable binary images from grayscale source material.
- </p>
- {% if 'render_job' in session %}
- <strong>Processing...</strong> (this may take several minutes!)
- {% else %}
- <img src="{{url_for('render_preview', side='top')}}"> <a href="{{url_for('render_download', side='top')}}">Download</a>
- <img src="{{url_for('render_preview', side='bottom')}}"> <a href="{{url_for('render_download', side='bottom')}}">Download</a>
- {% endif %}
- <form method="POST" action="{{url_for('session_reset')}}">
- {{reset_form.csrf_token}}
- <input type="submit" value="Start over">
- </form>
- </div>
+ <div class="controls">
+ <form id="gerber-upload-form" method="POST" action="{{url_for('upload', namespace='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>
- <div class="step" id="step3">
- <h2>Step 3: 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>
- <form method="POST" action="{{url_for('upload', namespace='overlay')}}" enctype="multipart/form-data">
- {{overlay_form.csrf_token}}
- {{overlay_form.upload_file.label}} {{overlay_form.upload_file(size=20)}}
- {{overlay_form.side.label}} {{overlay_form.side()}}
- <input type="submit" value="Submit">
- </form>
- </div>
+ {% if 'render_job' in session or has_renders %}
+ <div class="step" id="step2">
+ <div class="description">
+ <h2>Step 2: 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.
- {% if 'vector_job' in session or has_output %}
- <div class="step" id="step4">
- <h2> Step 4: Download the processed gerber files</h2>
- {% if 'vector_job' in session %}
- <strong>Processing...</strong> (this may take several minutes!)
- {% else %}
- <a href="{{url_for('output_download')}}">Download</a>
- {% endif %}
- <form method="POST" action="{{url_for('session_reset')}}">
- {{reset_form.csrf_token}}
- <input type="submit" value="Start over">
- </form>
- </div>
- {% endif %} {# vector job #}
- {% endif %} {# render job #}
- </body>
+ 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 %}
+ <strong>Processing...</strong> (this may take several minutes!)
+ {% else %}
+ <div class="preview-images">
+ <div class="preview preview-top" style="background-image:url('{{url_for('render_preview', side='top')}}');">
+ <a class="overlay" href="{{url_for('render_download', side='top')}}" onclick="document.querySelector('#side-0').checked=true">Download<br/>top layer</a>
+ </div>
+ <div class="preview preview-bottom" style="background-image:url('{{url_for('render_preview', side='bottom')}}');">
+ <a class="overlay" href="{{url_for('render_download', side='bottom')}}" onclick="document.querySelector('#side-1').checked=true">Download<br/>bottom layer</a>
+ </div>
+ </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>Step 3: 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', namespace='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> Step 4: Download the processed gerber files</h2>
+ </div>
+ <div class="controls">
+ {# if 'vector_job' in session FIXME #}
+ {% if True %}
+ <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>
+ </div>
+ </div>
+ {% endif %} {# vector job #}
+ {% endif %} {# render job #}
+ </div>
+ </div>
+ </body>
</html>