gerboweb: Initial design revision

This commit is contained in:
jaseg 2019-03-30 04:01:03 +09:00 committed by jaseg
parent 23d392c2f7
commit 6b4eac36d6
9 changed files with 206 additions and 77 deletions

View file

@ -13,8 +13,8 @@ rm -f /mnt/render_top.png /mnt/render_bottom.png /mnt/render_top.small.png /mnt/
date; echo 'Rendering bottom layer'
gerbolyze render top /tmp/gerber /mnt/render_top.png
date; echo 'Scaling down'
convert /mnt/render_top.png -resize 500x500 /mnt/render_top.small.png
convert /mnt/render_top.png -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/render_top.small.png
date; echo 'Rendering top layer'
gerbolyze render bottom /tmp/gerber /mnt/render_bottom.png
date; echo 'Scaling down'
convert /mnt/render_bottom.png -resize 500x500 /mnt/render_bottom.small.png"
convert /mnt/render_bottom.png -resize 500x500 -negate -brightness-contrast 30x30 -colorspace gray /mnt/render_bottom.small.png"

View file

@ -26,7 +26,8 @@ class UploadForm(FlaskForm):
class OverlayForm(UploadForm):
upload_file = FileField(validators=[FileRequired()])
side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')], default=lambda: session.get('last_download'))
side = RadioField('Side', choices=[('top', 'Top'), ('bottom', 'Bottom')],
default=lambda: session.get('side_selected', session.get('last_download')))
class ResetForm(FlaskForm):
pass
@ -56,7 +57,6 @@ 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(),
@ -79,37 +79,49 @@ def index():
# * The uploaded files are deleted after a while by systemd tmpfiles.d
# TODO: validate this setting applies *after* gzip transport compression
@app.route('/upload/<namespace>', methods=['POST'])
@require_session_id
def upload(namespace):
if namespace not in ('gerber', 'overlay'):
return abort(400, 'Invalid upload type')
def vectorize():
if 'vector_job' in session:
job_queue.drop(session['vector_job'])
session['vector_job'] = job_queue.enqueue('vector',
client=request.remote_addr,
session_id=session['session_id'],
side=session['side_selected'])
upload_form = UploadForm() if namespace == 'gerber' else OverlayForm()
def render():
if 'render_job' in session:
job_queue.drop(session['render_job'])
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
if namespace == 'gerber':
f.save(tempfile_path('gerber.zip'))
session['filename'] = secure_filename(f.filename) # Cache filename for later download
if 'render_job' in session:
job_queue.drop(session['render_job'])
session['render_job'] = job_queue.enqueue('render',
session_id=session['session_id'],
client=request.remote_addr)
else: # namespace == 'vector'
f.save(tempfile_path('overlay.png'))
render()
if path.isfile(tempfile_path('overlay.png')): # Re-vectorize when gerbers change
vectorize()
# Re-vectorize if either file has changed
if path.isfile(tempfile_path('gerber.zip')) and path.isfile(tempfile_path('overlay.png')):
if 'vector_job' in session:
job_queue.drop(session['vector_job'])
session['vector_job'] = job_queue.enqueue('vector',
client=request.remote_addr,
session_id=session['session_id'],
side=upload_form.side.data)
flash(f'Gerber file successfully uploaded.', 'success')
return redirect(url_for('index'))
flash(f'{"Gerber" if namespace == "gerber" else "Overlay"} file successfully uploaded.', 'success')
@app.route('/upload/overlay', methods=['POST'])
@require_session_id
def upload_overlay():
upload_form = OverlayForm()
if upload_form.validate_on_submit():
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>')
@ -144,5 +156,6 @@ def session_reset():
if 'vector_job' in session:
session['vector_job'].abort()
session.clear()
flash('Session reset', 'success');
return redirect(url_for('index'))

BIN
gerboweb/static/bg.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 KiB

BIN
gerboweb/static/bg10.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
gerboweb/static/sample1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 293 KiB

BIN
gerboweb/static/sample2.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

BIN
gerboweb/static/sample3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

View file

@ -35,30 +35,61 @@
--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(--c-metallic4);
color: var(--cb1);
display: flex;
flex-direction: row;
justify-content: center;
margin: 0;
background-color: hsl(10 10% 97%);
background-color: var(--cb8);
}
.layout-container {
flex-basis: 55em;
flex-shrink: 1;
flex-grow: 0;
padding: 3em;
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(--c-green1);
color: hsl(80 20% 20%);
text-shadow: 0 0 2px var(--c-green1);
background-color: var(--cg6);
color: var(--cg1);
text-shadow: 0 0 2px var(--cg7);
border-radius: 5px;
margin: 1em;
padding-left: 3em;
@ -79,6 +110,13 @@ div.flash-success::before {
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 {
@ -89,6 +127,7 @@ div.loading-message {
.steps {
display: flex;
flex-direction: column;
counter-reset: step;
}
.step {
@ -99,6 +138,29 @@ div.loading-message {
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: 50px;
padding-top: 10px;
}
.step > .description {
@ -115,7 +177,9 @@ div.loading-message {
.step > .description > h2 {
text-align: right;
margin-top: 0
margin-top: 0;
padding-left: 60px;
height: 60px;
}
.step > .controls {
@ -124,28 +188,28 @@ div.loading-message {
display: flex;
flex-direction: column;
align-items: stretch;
margin-right: 20px;
margin-left: 20px;
margin-right: 1em;
margin-left: 1em;
padding: 1em;
background-color: hsl(210 40% 97%);
background-color: var(--cb8);
border-radius: 5px;
}
input.reset-button {
background-color: var(--c-red1);
color: var(--c-grey1);
text-shadow: 0 0 2px var(--c-red3);
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(--c-green2);
color: hsl(80 20% 20%);
text-shadow: 0 0 2px var(--c-green1);
background-color: var(--cg4);
color: var(--cg1);
text-shadow: 0 0 2px var(--cg7);
font-weight: bold;
margin-left: 1em;
border: 0;
@ -173,8 +237,8 @@ input.submit-button {
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);
color: var(--cb1);
text-shadow: 0.5px 0.5px 0.5px var(--cb6);
}
.preview-images {
@ -185,21 +249,56 @@ a.output-download:link, a.output-download:hover, a.output-download:visited, a.ou
justify-content: space-around;
}
.preview {
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;
}
a.overlay:link, a.overlay:hover, a.overlay:visited, a.overlay:active {
.overlay {
text-align: center;
font-size: 30pt;
font-size: 50pt;
font-weight: bold;
color: var(--c-metallic4);
text-shadow: 0.5px 0.5px 0.5px var(--c-metallic2);
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/ */
@ -216,10 +315,10 @@ a.overlay:link, a.overlay:hover, a.overlay:visited, a.overlay:active {
width: 51px;
height: 51px;
margin: 6px;
border: 6px solid var(--c-metallic4);
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(--c-metallic4) transparent transparent transparent;
border-color: var(--cb1) transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;

View file

@ -6,14 +6,16 @@
</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 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) %}
@ -31,7 +33,7 @@
<div class="steps">
<div class="step" id="step1">
<div class="description">
<h2>Step 1: Upload zipped gerber files</h2>
<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.
@ -39,7 +41,7 @@
</div>
<div class="controls">
<form id="gerber-upload-form" method="POST" action="{{url_for('upload', namespace='gerber')}}" enctype="multipart/form-data">
<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">
@ -56,7 +58,7 @@
{% 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>
<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.
@ -68,15 +70,19 @@
</div>
<div class="controls">
{% if 'render_job' in session %}
<strong>Processing...</strong> (this may take several minutes!)
<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">
<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>
<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">
@ -87,7 +93,7 @@
<div class="step" id="step3">
<div class="description">
<h2>Step 3: Upload overlay image</h2>
<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
@ -95,7 +101,7 @@
</p>
</div>
<div class="controls">
<form id="overlay-upload-form" method="POST" action="{{url_for('upload', namespace='overlay')}}" enctype="multipart/form-data">
<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">
@ -119,11 +125,10 @@
{% 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>
<h2>Download the processed gerber files</h2>
</div>
<div class="controls">
{# if 'vector_job' in session FIXME #}
{% if True %}
{% 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>
@ -137,11 +142,23 @@
<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>