From 638228a88d15ae1c36e3b1c1455a5789a08ff9bb Mon Sep 17 00:00:00 2001 From: Ignacio Rivero Date: Tue, 24 Jun 2025 01:36:59 -0300 Subject: [PATCH] Added 4bpp option --- app.py | 821 +++++++++++++++++++++++++++++++-------------------------- 1 file changed, 445 insertions(+), 376 deletions(-) diff --git a/app.py b/app.py index 013a437..d83b7f1 100644 --- a/app.py +++ b/app.py @@ -36,392 +36,443 @@ DITHERING_MODES = { HTML_FORM = ''' -CatNote - - -
-
-
-

😺 CatNote 🖨️

-
-
-
-
-
-
-
- - + input[type=number]::-webkit-inner-spin-button, + input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; + } + .options-dithering, + .options-intensity, + .options-rotation, + .options-printmode { + display: flex; + flex-direction: column; + } + #loading-box { + display: none; + flex-direction: column; + text-align: center; + box-sizing: border-box; + width: 100%; + padding: 1em; + } + #printing-text { + color: #eee; + font-size: 1em; + font-weight: bold; + margin-bottom: 1em; + } + .progress-bar { + position: relative; + width: 100%; + height: 18px; + background: #181c1f; + border: 1.5px solid #444; + border-radius: 12px; + overflow: hidden; + box-shadow: 0 2px 12px #1116; + } + .progress-bar-inner { + width: 40%; + height: 100%; + background: linear-gradient(90deg, #8ee3c1, #35a7ff); + border-radius: 12px; + position: absolute; + left: -40%; + animation: indeterminate-bar 1.2s infinite + } + @keyframes indeterminate-bar { + 0% { left: 0%; } + 50% { left: calc(100% - 40%); } + 100% { left: 0%; } + } + @media (max-width: 1000px) { + body { + font-size: 1.28em !important; + } + h2, h3, h4 { + font-size: 2em !important; + } + .centered-flex { + flex-direction: column; + align-items: center; + height: auto; + gap: 1.6em; + } + .form-card, + .preview-card, + .markdown-ref { + font-size: 1.18em !important; + min-width: auto; + max-width: 90vw; + width: 100%; + margin: 0.8em 0; + } + textarea { + font-size: 1.16em !important; + padding: 1em; + min-height: 12em; + } + .status-msg { + font-size: 1.25em !important; + padding: 0.8em 1.6em; + } + button[type=submit] { + font-size: 1.3em !important; + padding: 0.9em 2.2em; + } + input[type=file] { + font-size: 1.16em !important; + padding: inherit 2.2em; + } + label { + 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; + } + } + + + +
+
+
+

😺 CatNote 🖨️

+
+ +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
Imprimiendo...
+
+
+
+
-
- - +
+ +
-
-
- - -
- - {% if printed %} -
✅ Enviado a impresora
- {% endif %} - {% if error %} -
⚠️ {{ error }}
- {% endif %} + + {% if printed %} +
✅ Enviado a impresora
+ {% endif %} + {% if error %} +
⚠️ {{ error }}
+ {% endif %} +
+
+ {% if img %} +

Vista previa

+ + {% else %} +

Su vista previa aparecerá aquí

+ {% endif %} +
+
+

Referencia rápida de Markdown

+
    +
  • Negrita: **texto**
  • +
  • Cursiva: *texto*
  • +
  • Negrita y cursiva: ***texto***
  • +
  • Encabezado grande: # Título
  • +
  • Encabezado mediano: ## Título
  • +
  • Encabezado chico: ### Título
  • +
  • Lista con viñetas: - Elemento
  • +
  • Lista numerada: 1. Elemento
  • +
  • Imágen: ![Texto alternativo](Enlace a imágen)
  • +
  • Salto de línea: Deje una línea vacía
  • +
  • Imagen subida: !(img) (usa la imagen cargada abajo)
  • +
+
-
- {% if img %} -

Vista previa

- - {% else %} -

Su vista previa aparecerá aquí

- {% endif %} -
-
-

Referencia rápida de Markdown

-
    -
  • Negrita: **texto**
  • -
  • Cursiva: *texto*
  • -
  • Negrita y cursiva: ***texto***
  • -
  • Encabezado grande: # Título
  • -
  • Encabezado mediano: ## Título
  • -
  • Encabezado chico: ### Título
  • -
  • Lista con viñetas: - Elemento
  • -
  • Lista numerada: 1. Elemento
  • -
  • Imágen: ![Texto alternativo](Enlace a imágen)
  • -
  • Salto de línea: Deje una línea vacía
  • -
  • Imagen subida: !(img) (usa la imagen cargada abajo)
  • -
-
-
+ + + ''' def remove_uploaded_img(): @@ -432,11 +483,11 @@ def remove_uploaded_img(): except Exception: pass -def bleh_image_from_url(url, dithering): +def bleh_image_from_url(url, dithering, mode): resp = requests.get(url, stream=True) resp.raise_for_status() bleh = subprocess.Popen( - ["./bleh", "-o", "-", "-mode", "1bpp", "-d", f"{dithering}", "-"], + ["./bleh", "-o", "-", "-mode", f"{mode}", "-d", f"{dithering}", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = bleh.communicate(resp.content) if bleh.returncode != 0: @@ -447,9 +498,9 @@ def bleh_image_from_url(url, dithering): img = img.resize((IMAGE_WIDTH, img.height), Image.LANCZOS) return img -def bleh_image_from_bytes(image_bytes, dithering): +def bleh_image_from_bytes(image_bytes, dithering, mode): bleh = subprocess.Popen( - ["./bleh", "-o", "-", "-mode", "1bpp", "-d", f"{dithering}", "-"], + ["./bleh", "-o", "-", "-mode", f"{mode}", "-d", f"{dithering}", "-"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) out, err = bleh.communicate(image_bytes) @@ -460,6 +511,15 @@ def bleh_image_from_bytes(image_bytes, dithering): img = img.resize((IMAGE_WIDTH, img.height), Image.LANCZOS) return img +def rotate_image_bytes(image_bytes, rotation): + if rotation == 0: + return image_bytes + img = Image.open(io.BytesIO(image_bytes)) + img = img.rotate(-rotation, expand=True) # PIL rotates counterclockwise, negative for clockwise + buf = io.BytesIO() + img.save(buf, format="PNG") + return buf.getvalue() + def parse_line(line): if re.match(r"!\(img\)", line) or re.match(r"!\[.*\]\(img\)", line): return ('userimage', None) @@ -559,7 +619,7 @@ def wrap_segments(segments, font, font_bold, font_italic, font_bolditalic, max_w if line: yield line -def render(md, dithering, uploaded_img_bytes=None): +def render(md, dithering, printmode, 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) @@ -578,14 +638,14 @@ def render(md, dithering, uploaded_img_bytes=None): tag = parse_line(src_line) if tag[0] == 'image': try: - image = bleh_image_from_url(tag[1], dithering) + image = bleh_image_from_url(tag[1], dithering, printmode) 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, dithering) + image = bleh_image_from_bytes(uploaded_img_bytes, dithering, printmode) lines_out.append(('image', image)) except Exception as e: lines_out.append(('text', [('text', f"[Error al procesar imagen]")], font, FONT_SIZE)) @@ -714,6 +774,7 @@ def index(): if request.method == "POST": userimg = request.files.get("userimg") uploaded_img_bytes = None + rotation = int(request.form.get("rotation", "0")) if userimg and userimg.filename: # Remove old temp file if present remove_uploaded_img() @@ -734,10 +795,19 @@ def index(): session.pop('uploaded_img_path', None) else: remove_uploaded_img() + if uploaded_img_bytes and rotation in (90, 180, 270): + try: + uploaded_img_bytes = rotate_image_bytes(uploaded_img_bytes, rotation) + except Exception as e: + error = f"Error rotating image: {e}" + uploaded_img_bytes = None md = request.form["md"] dithering = request.form.get("dithering", "floyd") - image = render(md, dithering, uploaded_img_bytes) + printmode = request.form.get("printmode", "1bpp") + image = render(md, dithering, printmode, uploaded_img_bytes) session['dithering'] = dithering + session['printmode'] = printmode + session['rotation'] = rotation buf = io.BytesIO() image.save(buf, format="PNG") buf.seek(0) @@ -750,7 +820,6 @@ def index(): 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: @@ -761,7 +830,7 @@ def index(): # Run the bleh command result = subprocess.run([ "./bleh", - "-mode", "1bpp", + "-mode", f"{printmode}", "-intensity", f"{intensity}", tmpfile.name ], capture_output=True, text=True, timeout=90) @@ -772,7 +841,7 @@ def index(): except Exception as e: error = f"Failed to print: {e}" 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')) + printed=printed, error=error, current_dithering=session.get('dithering', 'floyd'), current_rotation=session.get('rotation', 0), current_printmode=session.get('printmode', '1bpp')) @app.route('/manifest.json') def manifest():