diff --git a/gerboweb/deploy/render.sh b/gerboweb/deploy/render.sh index c3920de..eefe7f0 100755 --- a/gerboweb/deploy/render.sh +++ b/gerboweb/deploy/render.sh @@ -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" diff --git a/gerboweb/gerboweb.py b/gerboweb/gerboweb.py index 1f8d884..a276d74 100644 --- a/gerboweb/gerboweb.py +++ b/gerboweb/gerboweb.py @@ -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/', 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/') @@ -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')) diff --git a/gerboweb/static/bg.jpg b/gerboweb/static/bg.jpg new file mode 100644 index 0000000..94856fc Binary files /dev/null and b/gerboweb/static/bg.jpg differ diff --git a/gerboweb/static/bg10.jpg b/gerboweb/static/bg10.jpg new file mode 100644 index 0000000..9d14fd3 Binary files /dev/null and b/gerboweb/static/bg10.jpg differ diff --git a/gerboweb/static/sample1.jpg b/gerboweb/static/sample1.jpg new file mode 100644 index 0000000..948da6f Binary files /dev/null and b/gerboweb/static/sample1.jpg differ diff --git a/gerboweb/static/sample2.jpg b/gerboweb/static/sample2.jpg new file mode 100644 index 0000000..ef47bd4 Binary files /dev/null and b/gerboweb/static/sample2.jpg differ diff --git a/gerboweb/static/sample3.jpg b/gerboweb/static/sample3.jpg new file mode 100644 index 0000000..780c080 Binary files /dev/null and b/gerboweb/static/sample3.jpg differ diff --git a/gerboweb/static/style.css b/gerboweb/static/style.css index 975c7f2..ede89d4 100644 --- a/gerboweb/static/style.css +++ b/gerboweb/static/style.css @@ -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; diff --git a/gerboweb/templates/index.html b/gerboweb/templates/index.html index 3e4c255..eeece65 100644 --- a/gerboweb/templates/index.html +++ b/gerboweb/templates/index.html @@ -6,14 +6,16 @@
-
-

Raster image to PCB converter

-

- 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. -

+
+
+

Raster image to PCB converter

+

+ 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. +

+
{% with messages = get_flashed_messages(with_categories=True) %} @@ -31,7 +33,7 @@
-

Step 1: Upload zipped gerber files

+

Upload zipped gerber files

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 @@

-
+ {{gerber_form.csrf_token}}
@@ -56,7 +58,7 @@ {% if 'render_job' in session or has_renders %}
-

Step 2: Download the target side's preview image

+

Download the target side's preview image

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 @@

{% if 'render_job' in session %} - Processing... (this may take several minutes!) +
+
+
Processing...
+
(this may take several minutes!)
+
{% else %} {% endif %}
@@ -87,7 +93,7 @@
-

Step 3: Upload overlay image

+

Upload overlay image

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. Antialiased @@ -95,7 +101,7 @@

-
+ {{overlay_form.csrf_token}}
@@ -119,11 +125,10 @@ {% if 'vector_job' in session or has_output %}
-

Step 4: Download the processed gerber files

+

Download the processed gerber files

- {# if 'vector_job' in session FIXME #} - {% if True %} + {% if 'vector_job' in session %}
Processing...
@@ -137,11 +142,23 @@
+
{% endif %} {# vector job #} {% endif %} {# render job #}
+
+

Sample images

+ + + +