gerboweb: Initial design revision
This commit is contained in:
parent
23d392c2f7
commit
6b4eac36d6
9 changed files with 206 additions and 77 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
BIN
gerboweb/static/bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
BIN
gerboweb/static/bg10.jpg
Normal file
BIN
gerboweb/static/bg10.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
gerboweb/static/sample1.jpg
Normal file
BIN
gerboweb/static/sample1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 293 KiB |
BIN
gerboweb/static/sample2.jpg
Normal file
BIN
gerboweb/static/sample2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 246 KiB |
BIN
gerboweb/static/sample3.jpg
Normal file
BIN
gerboweb/static/sample3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue