diff options
-rw-r--r-- | gerboweb/deploy/nginx.conf | 2 | ||||
-rw-r--r-- | gerboweb/deploy/playbook.yml | 6 | ||||
-rw-r--r-- | gerboweb/deploy/uwsgi-gerboweb.ini | 1 | ||||
-rw-r--r-- | gerboweb/gerboweb.py | 10 | ||||
-rw-r--r-- | gerboweb/static/style.css | 241 | ||||
-rw-r--r-- | gerboweb/templates/index.html | 217 |
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> |