11 Commits

Author SHA1 Message Date
n07070
98299cc281 Add information on contributions 2026-01-04 12:08:05 +01:00
n07070
d261bb0fdc Add 3d models of the printer and raspberry pi 2026-01-02 17:40:29 +01:00
n07070
46fda975d4 Ajout des logos de extase club 2025-11-13 16:59:52 +01:00
nono
670ab495d8 Update code to better handle the raspberry pi mode 2025-10-17 17:33:53 +02:00
nono
34bffe93af Add systemd service 2025-10-17 17:33:37 +02:00
nono
8ecb93656c Change signature 2025-10-16 18:39:29 +02:00
nono
eac737b889 Remove flash from the printer class, update web class 2025-10-16 18:36:16 +02:00
nono
dfeb1be0f0 update raspberry pi code 2025-10-16 18:35:57 +02:00
nono
0eefafdeb2 Add configuration ports for the flash 2025-10-16 18:35:35 +02:00
nono
ce70d498ca Add requirements 2025-10-16 18:35:27 +02:00
nono
991f6794c7 Update readme with more deps 2025-10-16 18:35:16 +02:00
13 changed files with 534 additions and 143 deletions

View File

@@ -20,7 +20,7 @@ To make this project work, you will need :
- Some knowledge of the command line,
- Some knowledge of Python.
- 3h of your time, 5h if things need debugging.
- `git`, `virtualenv`,`pip` and `python` >= 3.8.6.
- `fswebcam`, `libjpeg-dev` ,`zlib1g-dev`,`libffi-dev`,`git`, `virtualenv`,`pip` and `python` >= 3.8.6.
- A webcam for the webcam page to work. Will work on a smartphone. Not required.
## Context
@@ -61,6 +61,12 @@ This command should start a web server with which you can test your configuratio
Voilà !
## Contributions
Your contributions are very much welcome ! You can either request an account on git.n07070.xyz, or send me a patch by email ( see git-send-mail.io ). Please [squash](https://www.geeksforgeeks.org/git/use-of-git-squash-commits/) yours commits into one commit, and add as much information in the commit's description. The more you add comments and descriptions, the better it is.
Please also say if you had a printer to test your code, and which printer you've been using.
## Screenshots
![](src/static/images/homepage.png)

View File

@@ -8,8 +8,9 @@ upload_folder = "src/static/uploads"
# Raspberry Pi Configuration
[rpi]
button_gpio_port_number = 17
indicator_gpio_port_number = 18
button_gpio_port_number = 2
indicator_gpio_port_number = 22
flash_gpio_port_number = 16
flash = true
# Users = Password

Binary file not shown.

Binary file not shown.

12
littleprynter.service Normal file
View File

@@ -0,0 +1,12 @@
[Unit]
Description=LittlePrynter
After=network.target
[Service]
Type=simple
WorkingDirectory=/home/pi/littleprynter/
Environment=FLASK_APP=src/main.py
ExecStart=/home/pi/littleprynter/bin/flask run --host 0.0.0.0 --debug --no-reload
[Install]
WantedBy=multi-user.target

View File

@@ -1,36 +1,261 @@
Adafruit_Thermal>=1.1.0
appdirs>=1.4.4
argcomplete>=2.0.6
cffi>=1.17.1
click>=8.1.8
commonmark>=0.9.1
cryptography>=45.0.4
Deprecated>=1.2.18
escpos>=2.0.0
Flask>=2.1.3
Flask-Limiter>=2.4.5.1
future>=0.18.3
itsdangerous>=2.1.2
Jinja2>=3.1.6
limits>=2.6.3
MarkupSafe>=2.1.5
numpy>=2.3.0
packaging>=21.3
pillow>=11.2.1
pycparser>=2.22
Pygments>=2.12.0
pyparsing>=3.0.9
pyserial>=3.5
python-barcode>=0.13.1
pyusb>=1.2.1
PyYAML>=6.0.2
qrcode>=7.3.1
rich>=12.4.4
setuptools>=80.9.0
six>=1.16.0
toml>=0.10.2
typing_extensions>=4.2.0
Unidecode>=1.3.8
viivakoodi>=0.8.0
Werkzeug>=2.1.2
wrapt>=1.14.1
appdirs==1.4.4
apt-listchanges==4.8
argcomplete==3.6.2
arrow==1.3.0
attrs==25.3.0
babel==2.17.0
bcrypt==4.2.0
bidict==0.23.1
blinker==1.9.0
certifi==2025.1.31
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.2
click==8.3.0
cloud-init==25.2
colorzero==2.0
configobj==5.0.9
cryptography==43.0.0
Deprecated==1.2.18
distlib==0.3.9
distro==1.9.0
escpos==2.0.0
filelock==3.18.0
Flask==3.1.2
Flask-Limiter==4.0.0
Flask-SocketIO==5.5.1
fqdn==1.5.1
future==1.0.0
gpiod==2.2.0
gpiozero==2.0.1
h11==0.16.0
idna==3.10
isoduration==20.11.0
itsdangerous==2.2.0
Jinja2==3.1.6
jsonpatch==1.32
jsonpointer==2.4
jsonschema==4.19.2
jsonschema-specifications==2023.12.1
lgpio==0.2.2.0
limits==5.6.0
linkify-it-py==2.0.3
markdown-it-py==3.0.0
MarkupSafe==2.1.5
mdurl==0.1.2
numpy==2.3.4
oauthlib==3.2.2
olefile==0.47
ordered-set==4.1.0
packaging==25.0
pillow==11.1.0
platformdirs==4.3.7
ply==3.11
pycparser==2.22
pycryptodomex==3.20.0
Pygments==2.18.0
PyJWT==2.10.1
pyserial==3.5
python-apt==3.0.0
python-barcode==0.16.1
python-dateutil==2.9.0
python-engineio==4.12.3
python-socketio==5.14.1
pyusb==1.3.1
PyYAML==6.0.2
qrcode==8.2
referencing==0.36.2
requests==2.32.3
rfc3339-validator==0.1.4
rfc3986-validator==0.1.1
rfc3987==1.3.8
rich==13.9.4
rpds-py==0.21.0
rpi-keyboard-config==1.0
rpi-lgpio==0.6
setuptools==80.9.0
simple-websocket==1.1.0
six==1.17.0
smbus2==0.4.3
spidev==3.6
ssh-import-id==5.10
toml==0.10.2
types-aiofiles==24.1
types-antlr4-python3-runtime==4.13
types-assertpy==1.1
types-atheris==2.3
types-aws-xray-sdk==2.14
types-beautifulsoup4==4.12
types-bleach==6.2
types-boltons==24.1
types-braintree==4.31
types-cachetools==5.5
types-caldav==1.3
types-capturer==3.0
types-cffi==1.16
types-chevron==0.14
types-click-default-group==1.2
types-click-spinner==0.1
types-colorama==0.4
types-commonmark==0.9
types-console-menu==0.8
types-corus==0.10
types-croniter==5.0.1
types-dateparser==1.2
types-decorator==5.1
types-defusedxml==0.7
types-Deprecated==1.2.15
types-docker==7.1
types-dockerfile-parse==2.0
types-docutils==0.21
types-editdistance==0.8
types-entrypoints==0.4
types-ExifRead==3.0
types-fanstatic==1.4
types-first==2.0
types-flake8==7.1
types-flake8-bugbear==24.12.12
types-flake8-builtins==2.5
types-flake8-docstrings==1.7
types-flake8-rst-docstrings==0.3
types-flake8-simplify==0.21
types-flake8-typing-imports==1.16
types-Flask-Cors==5.0
types-Flask-Migrate==4.0
types-Flask-SocketIO==5.4
types-fpdf2==2.8.2
types-gdb==15.0
types-gevent==24.11
types-google-cloud-ndb==2.3
types-greenlet==3.1
types-hdbcli==2.23
types-html5lib==1.1
types-httplib2==0.22
types-humanfriendly==10.0
types-hvac==2.3
types-ibm-db==3.2.4
types-icalendar==6.1
types-influxdb-client==1.45
types-inifile==0.4
types-JACK-Client==0.5
types-Jetson.GPIO==2.1
types-jmespath==1.0
types-jsonschema==4.23
types-jwcrypto==1.5
types-keyboard==0.13
types-ldap3==2.9
types-libsass==0.23
types-lupa==2.2
types-lzstring==1.0
types-m3u8==6.0
types-Markdown==3.7
types-mock==5.1
types-mypy-extensions==1.0
types-mysqlclient==2.2
types-nanoid==2.0.0
types-netaddr==1.3
types-netifaces==0.11
types-networkx==3.4.2
types-oauthlib==3.2
types-objgraph==3.6
types-olefile==0.47
types-openpyxl==3.1.5
types-opentracing==2.4
types-paramiko==3.5
types-parsimonious==0.10
types-passlib==1.7
types-passpy==1.0
types-peewee==3.17.8
types-pep8-naming==0.14
types-pexpect==4.9
types-pika-ts==1.3
types-polib==1.2
types-portpicker==1.6
types-protobuf==5.29.1
types-psutil==6.1
types-psycopg2==2.9.10
types-pyasn1==0.6
types-pyaudio==0.2
types-PyAutoGUI==0.9
types-pycocotools==2.0
types-pycurl==7.45.4
types-pyfarmhash==0.4
types-pyflakes==3.2
types-pygit2==1.15
types-Pygments==2.18
types-pyinstaller==6.11
types-pyjks==20.0
types-PyMySQL==1.1
types-pynput==1.7.7
types-pyOpenSSL==24.1
types-pyRFC3339==2.0.1
types-PyScreeze==1.0.1
types-pyserial==3.5
types-pysftp==0.2
types-pytest-lazy-fixture==0.6
types-python-crontab==3.2
types-python-datemath==3.0.1
types-python-dateutil==2.9
types-python-http-client==3.3.7
types-python-jenkins==1.8
types-python-jose==3.3
types-python-nmap==0.7
types-python-xlib==0.33
types-pytz==2024.2
types-pywin32==308
types-pyxdg==0.28
types-PyYAML==6.0
types-qrbill==1.1
types-qrcode==8.0
types-regex==2024.11.6
types-reportlab==4.2.5
types-requests==2.32
types-requests-oauthlib==2.0
types-retry==0.9
types-RPi.GPIO==0.7
types-s2clientprotocol==5
types-seaborn==0.13.2
types-Send2Trash==1.8
types-setuptools==75.6
types-shapely==2.0
types-simplejson==3.19
types-singledispatch==4.1
types-six==1.17
types-slumber==0.7
types-str2bool==1.1
types-tabulate==0.9
types-tensorflow==2.18.0
types-TgCrypto==1.2
types-toml==0.10
types-toposort==1.10
types-tqdm==4.67
types-translationstring==1.4
types-tree-sitter-languages==1.10
types-ttkthemes==3.2
types-ujson==5.10
types-unidiff==0.7
types-untangle==1.2
types-usersettings==1.1
types-uWSGI==2.0
types-vobject==0.9.9
types-waitress==3.0.1
types-WebOb==1.8
types-whatthepatch==1.0
types-workalendar==17.0
types-WTForms==3.2.1
types-wurlitzer==3.1
types-xdgenvpy==3.0
types-xmltodict==0.14
types-zstd==1.5
types-zxcvbn==4.4
typing_extensions==4.15.0
uc-micro-py==1.0.3
uritemplate==4.1.1
urllib3==2.3.0
viivakoodi==0.8.0
virtualenv==20.31.2
webcolors==1.13
Werkzeug==3.1.3
wheel==0.46.1
wrapt==1.17.3
wsproto==1.2.0

View File

@@ -77,10 +77,14 @@ printer = Printer(app,0x04b8, 0x0e28)
printer.init_printer()
# Find out if we are running on a Raspberry Pi
rpi = Raspberry(printer, app, socketio, configuration_file['rpi']['button_gpio_port_number'], configuration_file['rpi']['indicator_gpio_port_number'], configuration_file['rpi']['flash'] )
rpi = Raspberry(printer,
app,
socketio,
configuration_file['rpi']['button_gpio_port_number'], configuration_file['rpi']['indicator_gpio_port_number'],
configuration_file['rpi']['flash_gpio_port_number'],
configuration_file['rpi']['flash'] )
RASPBERRY_PI_CONNECTED = rpi.is_raspberry_pi()
if RASPBERRY_PI_CONNECTED:
rpi.initialise_gpio()
#############################################################
@@ -91,7 +95,7 @@ if RASPBERRY_PI_CONNECTED:
web = Web(app, printer)
if __name__ == "__main__":
app.run(ssl_context='adhoc')
app.run(debug=True, use_reloader=False, host='0.0.0.0', ssl_context='adhoc')
limiter = Limiter(
get_remote_address,

View File

@@ -1,5 +1,4 @@
# Importing the module to mage the connection to the printer.
from flask import flash
from escpos.printer import Usb, USBNotFoundError
from time import sleep, gmtime, strftime
import os.path
@@ -46,12 +45,10 @@ class Printer(object):
match status:
case 0:
self.app.logger.error('Printer has no more paper, aborting...')
flash("No more paper on the printer. Sorry.",category='error')
self.printer.close()
raise Exception("No more paper in the printer")
case 1:
self.app.logger.warning('Printer needs paper to be changed very soon ! ')
flash('Printer needs paper to be changed very soon ! ', category='info')
self.printer.close()
case 2:
self.app.logger.debug('Printer has paper, good to go')
@@ -101,47 +98,37 @@ class Printer(object):
return True
def print_sms(self, msg, signature) -> None:
clean_msg = str(msg)
def print_sms(self, msg, signature="",bold=False):
clean_msg = str(msg) + "\n"
clean_signature = str(signature)
if len(clean_msg) > 4096 or len(clean_msg) < 3 :
if len(clean_msg) > 4096:
self.app.logger.warning("Could not print message of this length: " + str(len(clean_msg)))
raise Exception("Could not print message of this length :" + str(len(clean_msg)) + ", needs to between 3 and 4096 caracters long.")
raise Exception("Could not print message of this length :" + str(len(clean_msg)) + ", needs to be below 4096 caracters long.")
if len(signature) > 256 or len(signature) < 3:
if len(signature) > 256:
self.app.logger.warning("Could not print signature of this length: " + str(len(clean_signature)))
raise Exception("Could not print signature of this length :" + str(len(clean_signature)) + ", needs to between 3 and 256 caracters long.")
self.check_paper()
raise Exception("Could not print signature of this length :" + str(len(clean_signature)) + ", needs to be below 256 caracters long.")
try:
self.printer.open(self.usb_args);
self.printer.set(align='left', font='a', bold=False, underline=0, width=1, height=1, density=8, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False)
self.printer.set(align='left', font='a', bold=True, underline=1, width=1, height=1, density=6, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False)
self.printer.textln(clean_msg)
self.printer.textln("")
self.printer.set(align='left', font='b', bold=False, underline=1, width=1, height=1, density=9, invert=False, smooth=True, flip=False, double_width=False, double_height=False, custom_size=False)
self.printer.textln("> " + clean_signature + " @ " + strftime("%Y-%m-%d %H:%M:%S", gmtime()))
self.printer.textln("")
self.printer.textln("Printed by LittlePrinter ")
self.printer.textln("n07070.xyz/articles/littleprynter")
self.printer.textln("")
self.printer.cut()
self.printer.set(align='center', font='a', bold=bold)
self.printer.textln(clean_msg )
if clean_signature:
self.printer.textln(clean_signature )
self.printer.close()
except Exception as e:
self.app.logger.error("Unable to print because : " + str(e))
raise e
flash("Message printed : " + clean_msg ,category='info')
self.app.logger.info("Printed text")
return True
def print_img(self, path, sign) -> None:
def print_img(self, path, sign="",center=True,process=False):
clean_signature = str(sign)
if len(sign) > 256 or len(sign) < 3:
if len(sign) > 256:
self.app.logger.warning("Could not print signature of this length: " + str(len(clean_signature)))
raise Exception("Could not print signature of this length :" + str(len(clean_signature)) + ", needs to between 3 and 256 caracters long.")
raise Exception("Could not print signature of this length :" + str(len(clean_signature)) + ", needs to be below 256 caracters long.")
if not os.path.isfile(str(path)):
self.app.logger.warning("File does not exist : " + str(path))
@@ -149,31 +136,61 @@ class Printer(object):
else:
self.app.logger.debug("Printing file from " + str(path))
if process:
try:
self.app.logger.debug("Proccessing the image")
path = process_image(self, path)
except Exception as e:
self.app.logger.error(str(e))
raise e
return False
else:
self.app.logger.warning("Not proccessing the image")
try:
self.check_paper()
self.printer.open(self.usb_args)
self.printer.textln("Printed by LittlePrinter ")
self.printer.textln("n07070.xyz/articles/littleprynter")
self.printer.textln("> " + clean_signature + " @ " + strftime("%Y-%m-%d %H:%M:%S", gmtime()))
self.printer.image(path)
self.printer.cut()
self.printer.image(path,center=center)
self.printer.close()
self.app.logger.debug("Printed an image : " + str(path))
os.remove(path)
self.app.logger.debug("Removed image.")
self.app.logger.debug("Removed image : " + str(path))
except Exception as e:
self.printer.close()
self.app.logger.error(str(e))
return False
self.app.logger.info("Printed a picture")
return True
def qr(self, content):
try:
self.printer.open(self.usb_args)
self.printer.qr(content, center=True)
self.printer.close()
except Exception as e:
self.printer.close()
self.app.logger.error(str(e))
return False
self.app.logger.info("Printed a QR")
return True
def cut(self):
try:
self.printer.open(self.usb_args)
self.printer.cut()
self.printer.close()
except Exception as e:
self.printer.close()
self.app.logger.error(str(e))
raise e
self.app.logger.info("Did a cut")
return True
def process_image(self, path):
brightness_factor = 1.5 # Used only if image is too dark
brightness_threshold = 100 # Brightness threshold (0255)
@@ -191,9 +208,9 @@ def process_image(self, path):
original_img.thumbnail((max_width, max_height), Image.LANCZOS)
self.app.logger.debug("Resized the image")
# Convert to grayscale for dithering
dithered_img = original_img.convert("L").convert("1") # Dithering using default method (FloydSteinberg)
self.app.logger.debug("Dithered the image")
# # Convert to grayscale for dithering
# dithered_img = original_img.convert("L").convert("1") # Dithering using default method (FloydSteinberg)
# self.app.logger.debug("Dithered the image")
# Compute brightness of original image (grayscale average)
grayscale = original_img.convert("L")
@@ -208,13 +225,13 @@ def process_image(self, path):
enhancer = ImageEnhance.Brightness(original_img)
original_img = enhancer.enhance(brightness_factor)
# Reduce contrast
contrast_enhancer = ImageEnhance.Contrast(original_img)
original_img = contrast_enhancer.enhance(contrast_factor)
# # Reduce contrast
# contrast_enhancer = ImageEnhance.Contrast(original_img)
# original_img = contrast_enhancer.enhance(contrast_factor)
# Final resize check
if original_img.height > max_height:
flash("Image is too long, sorry! Keep it below 575×1000 pixels.", 'error')
raise ValueError("Image is too long, sorry! Keep it below 575×1000 pixels.")
self.app.logger.error("Image is too long, sorry! Keep it below 575×1000 pixels.")
return False

View File

@@ -1,8 +1,10 @@
from flask_socketio import SocketIO
from gpiozero import Button, LED
from time import sleep
from gpiozero import Button, LED, DigitalOutputDevice
from time import sleep, gmtime, strftime
from PIL import Image
import io # To check if we are on a Raspberry Pi
import subprocess
import os
class Raspberry(object):
"""
@@ -13,24 +15,17 @@ class Raspberry(object):
- Flash an indicator light
"""
def __init__(self, printer, app, socketio, button_gpio_port_number, indicator_gpio_port_number, is_flash_present,):
def __init__(self, printer, app, socketio, button_gpio_port_number, indicator_gpio_port_number, flash_gpio_port_number, is_flash_present,):
self.printer = printer
self.socketio = socketio
self.app = app
self.flash_gpio = flash_gpio_port_number
self.is_flash_present = is_flash_present
self.button_gpio = button_gpio_port_number
self.led_gpio = indicator_gpio_port_number
self.image_path = self.app.config['UPLOAD_FOLDER'] + '/image.jpg'
def initialise_gpio(self):
self.button = Button(self.button_gpio)
self.led = LED(self.led_gpio)
button.when_pressed = self.on_button_pressed()
def is_raspberry_pi(self, raise_on_errors=False):
# Check if we are running on a raspberry pi
try:
@@ -59,48 +54,177 @@ class Raspberry(object):
return False
self.app.logger.debug('It seems we are on a Raspberry Pi')
return True
def take_picture(self):
# return a path to a picture
try:
subprocess.run(['fswebcam', '--no-banner', self.image_path])
self.initialise_gpio()
except Exception as e:
self.app.logger.error('Unable to take a picture :' + e)
self.app.logger.debug('Could not init GPIO : ' + str(e))
raise e
return True
def flash(state):
if state:
# turn on the flash
pass
else:
# turn off the flash
pass
def initialise_gpio(self):
self.app.logger.debug('Initializing GPIO')
def flash_indicator(self):
# The indicator will flash three times before return true
self.led.on()
sleep(0.1)
self.led.off()
sleep(1)
self.led.on()
sleep(0.1)
self.led.off()
sleep(1)
self.led.on()
sleep(0.1)
self.led.off()
self.led = LED(self.led_gpio)
self.app.logger.debug('Activated indicator LED')
self.indicator_countdown(iters=3)
self.button = Button(self.button_gpio, pull_up=True, bounce_time=0.1)
self.button.when_pressed = self.on_button_pressed
self.app.logger.debug('Activated button')
# The "flash" is a relay-controlled device ( light bulb for example )
self.flash = DigitalOutputDevice(self.flash_gpio)
self.flash_toggle()
self.app.logger.debug('Activated flash')
def indicator_countdown(self,iters=10,multi=10):
for i in range(iters,0,-1):
self.led.on()
sleep(i/multi)
self.led.off()
sleep(i/multi)
def indicator_led(self,timing=0.2,l=5):
for i in range(l):
self.app.logger.debug("LED turned on")
self.led.on()
sleep(timing)
self.led.off()
self.app.logger.debug("LED turned off")
sleep(timing)
def flash_toggle(self):
self.app.logger.debug("Flash turned on")
self.flash.on()
sleep(0.3)
self.flash.off()
self.app.logger.debug("Flash turned off")
def take_picture(self):
# Validate if the image path is valid
if not os.path.isdir(os.path.dirname(self.image_path)):
self.app.logger.error(f"Invalid directory for image path: {self.image_path}")
return False
def on_button_pressed():
self.socketio.emit('button_pressed') # Notify clients that a button has been pressed on the raspberry pi
self.flash_indicator() # The indicator will flash a countdown LED
self.flash(True)
try:
self.take_picture()
self.printer.print_image(self.image_path)
result = subprocess.run(
['fswebcam', '--no-banner', '-r', '1920x1080', self.image_path],
check=True, # Will raise CalledProcessError if the command fails
stdout=subprocess.PIPE, # Capture standard output
stderr=subprocess.PIPE # Capture error output
)
# Optionally log the command output
self.app.logger.debug(f"Image captured successfully: {result.stdout.decode()}")
except subprocess.CalledProcessError as e:
# Log error output if available
self.app.logger.error(f"Unable to take a picture. Error: {e.stderr.decode()}")
return False
except Exception as e:
app.logger.error("Could not take a picture after the button press : " + e)
# Catch any unexpected errors
self.app.logger.error(f"Unexpected error while taking picture: {str(e)}")
return False
# # Overlay logo
# logo_path = 'src/static/images/requin.png' # Update path as needed
# if not self.overlay_logo(self.image_path, logo_path):
# self.app.logger.warning("Picture taken but failed to overlay logo.")
return True
def overlay_logo(self, image_path, logo_path, output_path=None, position='bottom_right', margin=10):
try:
image = Image.open(image_path).convert("RGBA")
logo = Image.open(logo_path).convert("RGBA")
# Resize logo if it's too big (logo will be 30% the width of the image)
logo_ratio = 0.30 # You can change the ratio if you want the logo bigger or smaller
logo_width = int(image.width * logo_ratio)
logo_height = int(logo.height * (logo_width / logo.width))
logo = logo.resize((logo_width, logo_height), Image.Resampling.LANCZOS)
# Calculate position based on the chosen location
if position == 'bottom_right':
x = image.width - logo.width - margin
y = image.height - logo.height - margin
elif position == 'top_left':
x, y = margin, margin
elif position == 'top_right':
x = image.width - logo.width - margin
y = margin
elif position == 'bottom_left':
x = margin
y = image.height - logo.height - margin
else:
raise ValueError("Invalid position. Choose from 'bottom_right', 'top_left', 'top_right', or 'bottom_left'.")
# Composite the logo onto the image
image.paste(logo, (x, y), logo) # Use logo as its own alpha mask
# Save the result
if not output_path:
output_path = image_path # Overwrite the original image if no output path is given
image.save(output_path)
except Exception as e:
self.app.logger.error(f"Error overlaying logo: {e}")
return False
return True
def crop_to_square(self, image_path, output_path=None):
try:
image = Image.open(image_path)
width, height = image.size
# Determine shorter side
new_edge = min(width, height)
# Calculate cropping box (centered)
left = (width - new_edge) // 2
top = (height - new_edge) // 2
right = left + new_edge
bottom = top + new_edge
image = image.crop((left, top, right, bottom))
if not output_path:
output_path = image_path # Overwrite original
image.save(output_path)
except Exception as e:
self.app.logger.error(f"Error cropping image to square: {e}")
return False
return True
def on_button_pressed(self):
self.app.logger.debug("Button has been pressed")
self.led.on()
self.flash(False)
self.socketio.emit('picture_taken') # Notify clients that a picture has been taken
self.app.logger.debug("Counting down")
self.indicator_countdown(iters=4,multi=20) # The indicator will flash a countdown LED
self.app.logger.debug("Taking picture")
try:
self.flash.on()
self.take_picture()
except Exception as e:
self.app.logger.error("Could not take a picture after the button press : " + str(e))
finally:
self.flash.off()
self.app.logger.debug("Printing picture")
self.led.on()
self.crop_to_square(self.image_path)
self.printer.print_img('src/static/images/extase-club.png',process=True)
self.printer.print_img(self.image_path,process=True)
self.printer.print_sms("")
self.printer.print_sms("With Love From Société.Vide",signature="",bold=True)
self.printer.print_sms("Printed by LittlePrynter",signature="")
self.printer.print_sms("n07070.xyz",signature="")
self.printer.print_sms(strftime("%Y-%m-%d %H:%M", gmtime()),signature="")
self.printer.qr("https://n07070.xyz/articles/littleprynter")
self.printer.cut()
self.led.off()
self.app.logger.debug("Done printing picture")
return True

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -244,7 +244,7 @@ function print_picture(data){
let time = currentDate.getHours() + ":" + currentDate.getMinutes() + ":" + currentDate.getSeconds();
formData.set("img", picture, "picture.png");
formData.set("signature", " Accès Libre ~ 1 An ~ Fête de la Musique Libre @ " + time)
formData.set("signature", "Webcam")
fetch(url, {
method: 'POST', // or 'PUT'

View File

@@ -17,20 +17,22 @@ class Web(object):
self.app.logger.debug("Printing : " + str(texte) + " from " + str(sign))
try:
self.printer.print_sms(texte, sign)
self.printer.cut()
except Exception as e:
self.app.logger.error(e)
flash("Error while printing the SMS : "+ str(e))
flash("You message has been printed :)")
flash("You message " + str( texte ) + " has been printed :)")
def print_image(self, image, sign: str):
def print_image(self, image, sign):
self.app.logger.debug("Uploading file")
try:
self.app.logger.debug("Uploading file from " + str(sign))
if self.upload_file(image):
self.app.logger.debug("File has been uploaded, printing...")
self.printer.print_img(os.path.join(self.app.config['UPLOAD_FOLDER'], secure_filename(image.filename)), sign)
self.printer.print_img(os.path.join(self.app.config['UPLOAD_FOLDER'], secure_filename(image.filename)), sign=sign,process=True)
self.printer.cut()
except Exception as e:
self.app.logger.error(e)
flash("Could not upload file." + str(e))
@@ -57,7 +59,7 @@ class Web(object):
self.app.logger.debug("File saved")
except Exception as e:
self.app.logger.error("Could not save file")
flash(e,'error')
flash(str(e),'error')
return False
self.app.logger.debug("File saved to " + str(os.path.join(self.app.config['UPLOAD_FOLDER'], filename)))