From d21f5f662f28957de3008e35d805d42956d369bd Mon Sep 17 00:00:00 2001 From: Ignacio Rivero Date: Mon, 23 Jun 2025 23:00:29 -0300 Subject: [PATCH] Added dithering, intensity controls --- app.py | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 13 deletions(-) diff --git a/app.py b/app.py index 4c980d2..013a437 100644 --- a/app.py +++ b/app.py @@ -23,6 +23,17 @@ HEADER_SIZE_3 = 28 IMAGE_WIDTH = 384 BULLET_CHAR = "• " +DITHERING_MODES = { + "Sin dithering": "none", + "Floyd-Steinberg": "floyd", + "Bayer 2x2": "bayer2x2", + "Bayer 4x4": "bayer4x4", + "Bayer 8x8": "bayer8x8", + "Bayer 16x16": "bayer16x16", + "Atkinson": "atkinson", + "Jarvis-Judice-Ninke": "jjn" +} + HTML_FORM = ''' CatNote @@ -175,7 +186,7 @@ HTML_FORM = ''' label { font-size: 1em; - margin-left: 1em; + padding: 0 1em 0 1em; } input[type=file] { @@ -247,6 +258,51 @@ HTML_FORM = ''' min-height: 12em; } + .options { + display: flex; + flex-direction: column; + align-items: left; + justify-content: space-between; + margin-bottom: 1.2em; + } + + select { + background: #181c1f; + text-align: center; + text-align-last: center; + border: 1.5px solid #444; + color: #eee; + border-radius: 0.7em; + margin: 1em; + padding: 0.5em 1em; + } + + select option { + text-align: left; + } + + input[type=number] { + background: #181c1f; + text-align: center; + border: 1.5px solid #444; + color: #eee; + border-radius: 0.7em; + margin: 1em; + padding: 0.5em 1em; + } + + input[type=number]::-webkit-inner-spin-button, + input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + + .options-dithering, + .options-intensity { + display: flex; + flex-direction: column; + } + @media (max-width: 1000px) { body { font-size: 1.28em !important; /* Big bump */ @@ -293,6 +349,14 @@ HTML_FORM = ''' font-size: 1.16em !important; padding: inherit 2.2em } + select { + font-size: 1.16em !important; + padding: 0.6em 1.2em; + } + input[type=number] { + font-size: 1.16em !important; + padding: 0.6em 1.2em; + } } @@ -305,6 +369,20 @@ HTML_FORM = '''


+
+
+ + +
+
+ + +
+
@@ -354,11 +432,11 @@ def remove_uploaded_img(): except Exception: pass -def bleh_image_from_url(url): +def bleh_image_from_url(url, dithering): resp = requests.get(url, stream=True) resp.raise_for_status() bleh = subprocess.Popen( - ["./bleh", "-o", "-", "-mode", "1bpp", "-d", "floyd", "-"], + ["./bleh", "-o", "-", "-mode", "1bpp", "-d", f"{dithering}", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = bleh.communicate(resp.content) if bleh.returncode != 0: @@ -369,9 +447,9 @@ def bleh_image_from_url(url): img = img.resize((IMAGE_WIDTH, img.height), Image.LANCZOS) return img -def bleh_image_from_bytes(image_bytes): +def bleh_image_from_bytes(image_bytes, dithering): bleh = subprocess.Popen( - ["./bleh", "-o", "-", "-mode", "1bpp", "-d", "floyd", "-"], + ["./bleh", "-o", "-", "-mode", "1bpp", "-d", f"{dithering}", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) out, err = bleh.communicate(image_bytes) @@ -481,7 +559,7 @@ def wrap_segments(segments, font, font_bold, font_italic, font_bolditalic, max_w if line: yield line -def render(md, uploaded_img_bytes=None): +def render(md, dithering, uploaded_img_bytes=None): font = ImageFont.truetype(FONT_PATH, FONT_SIZE) font_bold = ImageFont.truetype(FONT_PATH_BOLD, FONT_SIZE) font_italic = ImageFont.truetype(FONT_PATH_OBLIQUE, FONT_SIZE) @@ -500,14 +578,14 @@ def render(md, uploaded_img_bytes=None): tag = parse_line(src_line) if tag[0] == 'image': try: - image = bleh_image_from_url(tag[1]) + image = bleh_image_from_url(tag[1], dithering) lines_out.append(('image', image)) except Exception as e: lines_out.append(('text', [('text', f"[Imagen inválida: {e}]")], font, FONT_SIZE)) elif tag[0] == 'userimage': if uploaded_img_bytes: try: - image = bleh_image_from_bytes(uploaded_img_bytes) + image = bleh_image_from_bytes(uploaded_img_bytes, dithering) lines_out.append(('image', image)) except Exception as e: lines_out.append(('text', [('text', f"[Error al procesar imagen]")], font, FONT_SIZE)) @@ -657,11 +735,23 @@ def index(): else: remove_uploaded_img() md = request.form["md"] - image = render(md, uploaded_img_bytes) + dithering = request.form.get("dithering", "floyd") + image = render(md, dithering, uploaded_img_bytes) + session['dithering'] = dithering buf = io.BytesIO() image.save(buf, format="PNG") buf.seek(0) img_data = base64.b64encode(buf.getvalue()).decode() + intensity = request.form.get("intensity", "85") + try: + intensity = int(intensity) + if intensity < 0 or intensity > 100: + raise ValueError("Intensity must be between 0 and 100") + except ValueError: + error = "Intensidad debe ser un número entre 0 y 100" + intensity = 85 + # Store intensity in session for later use + session['intensity'] = intensity # If print button pressed, send to driver if "print" in request.form: try: @@ -669,11 +759,10 @@ def index(): image.save(tmpfile, format="PNG") tmpfile.flush() # Run the bleh command - # You can set dither method here if desired result = subprocess.run([ "./bleh", "-mode", "1bpp", - "-intensity", "100", + "-intensity", f"{intensity}", tmpfile.name ], capture_output=True, text=True, timeout=90) if result.returncode != 0: @@ -682,8 +771,8 @@ def index(): printed = True except Exception as e: error = f"Failed to print: {e}" - return render_template_string(HTML_FORM, img=img_data, default_md=md, - printed=printed, error=error) + return render_template_string(HTML_FORM, dithering_modes=DITHERING_MODES, img=img_data, default_md=md, + printed=printed, error=error, current_dithering=session.get('dithering', 'floyd')) @app.route('/manifest.json') def manifest():