diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..9b077c2
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,46 @@
+FROM python:3.11.7 as base
+
+ENV PYTHONFAULTHANDLER=1 \
+ PYTHONHASHSEED=random \
+ PYTHONUNBUFFERED=1
+
+FROM base as builder
+
+ENV PIP_DEFAULT_TIMEOUT=100 \
+ PIP_DISABLE_PIP_VERSION_CHECK=1 \
+ PIP_NO_CACHE_DIR=1 \
+ # Poetry:
+ POETRY_NO_INTERACTION=1 \
+ POETRY_VIRTUALENVS_CREATE=false \
+ POETRY_CACHE_DIR='/var/cache/pypoetry' \
+ POETRY_HOME='/usr/local' \
+ POETRY_VERSION=1.7.1
+
+
+RUN apt-get update && apt-get upgrade -y \
+ && apt-get install --no-install-recommends -y \
+ bash \
+ brotli \
+ build-essential \
+ curl \
+ gettext \
+ git \
+ libpq-dev \
+ libpango-1.0.0 \
+ libpangocairo-1.0.0 \
+
+ # Installing `poetry`:
+ && curl -sSL 'https://install.python-poetry.org' | python - \
+ && poetry --version \
+ # Cleaning cache:
+ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
+ && apt-get clean -y && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /labelmaker
+COPY ./poetry.lock ./pyproject.toml /labelmaker/
+
+RUN poetry install --no-interaction --no-ansi --sync
+
+COPY . /labelmaker
+
+CMD poetry run python labelmaker/__main__.py
diff --git a/README.md b/README.md
index ecac4a5..3f51be9 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-Use the source, luke :^)
+Hackerspace label printing service
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..bb4f368
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,5 @@
+services:
+ labelmaker:
+ build: .
+ ports:
+ - "8080:5000"
diff --git a/render.py b/labelmaker/__main__.py
similarity index 65%
rename from render.py
rename to labelmaker/__main__.py
index d62d510..bc8611a 100644
--- a/render.py
+++ b/labelmaker/__main__.py
@@ -1,15 +1,13 @@
import json
import math
import os
-import StringIO
import subprocess
-import tempfile
import time
-import cairo
+import cairocffi as cairo
import flask
-import pango
-import pangocairo
+import pangocffi as pango
+import pangocairocffi as pangocairo
class App(flask.Flask):
def __init__(self, *args, **kwargs):
@@ -17,59 +15,57 @@ class App(flask.Flask):
self.last = 0
self.health = (0, False, "unknown")
-
app = App(__name__)
class Renderer(object):
INCH_PER_MM = 0.039
DPI = 300
- def __init__(self, size=(36,89)):
+ def __init__(self, size=(36, 89)):
width, height = [int(s * self.INCH_PER_MM * self.DPI) for s in size]
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
context = cairo.Context(surface)
- # fill it white, while we're at it
context.rectangle(0, 0, width, height)
context.set_source_rgb(1, 1, 1)
context.fill()
context.translate(width, 0)
context.rotate(math.pi/2)
- # yolo
+
self.width, self.height = height, width
+ self.font = pango.FontDescription()
self.context = context
self.surface = surface
def export_png(self, name):
- with open(name, 'w') as f:
+ with open(name, 'wb') as f:
self.surface.write_to_png(f)
def render_text(self, text, fontname, x, y, html=False):
+ print("Fontname:", fontname)
self.context.save()
if y != -1:
self.context.translate(x, y)
-
- pangocairo_context = pangocairo.CairoContext(self.context)
- pangocairo_context.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
- layout = pangocairo_context.create_layout()
- layout.set_width(self.width*pango.SCALE)
- layout.set_alignment(pango.ALIGN_CENTER)
-
+ self.context.set_antialias(cairo.ANTIALIAS_SUBPIXEL)
+ layout = pangocairo.create_layout(self.context)
+ layout._set_width(pango.units_from_double(self.width))
+ layout._set_alignment(pango.Alignment.CENTER)
if html:
# Absolutely horrifying hack to fix broken text wrapping
- layout.set_markup('%s' % (fontname, text))
+ layout.apply_markup('%s' % (fontname, text))
else:
- font = pango.FontDescription(fontname)
- layout.set_font_description(font)
- layout.set_text(text)
+ self.font.family = fontname.split()[0]
+ self.font.size = pango.units_from_double(int(fontname.split()[1]))
+ layout.font_description = self.font
+ layout.text = text
if y == -1:
- self.context.translate(0, (self.height - layout.get_size()[1]/pango.SCALE)/2)
+ self.context.translate(0, (self.height - pango.units_to_double(layout.get_size()[1]))/2)
self.context.set_source_rgb(0, 0, 0)
- pangocairo_context.update_layout(layout)
- pangocairo_context.show_layout(layout)
+ pangocairo.update_layout(self.context, layout)
+ pangocairo.show_layout(self.context, layout)
self.context.restore()
@@ -81,11 +77,11 @@ def healthcheck():
last_checked, last_status, last_details = app.health
if time.time() - last_checked < 1:
return last_status, last_details
- output = subprocess.check_output(['lpstat', '-p', '-d'])
+ output = subprocess.check_output(['lpstat', '-p', '-d']).decode()
mark = False
for line in output.split('\n'):
line = line.strip()
- if line.startswith('printer DYMO_LabelWriter_450'):
+ if line.startswith('printer DYMO_LabelWriter450'):
if 'is idle.' in line:
return True, 'Idle'
mark = True
@@ -98,31 +94,30 @@ def healthcheck():
app.health = (time.time(), False, line)
return False, line
mark = False
+ return False, "PRINTER_NOT_CONNECTED"
@app.route('/health')
def health():
ok, details = healthcheck()
return json.dumps({'ok': ok, 'details': details})
-@app.route('/stuff/preview//')
+@app.route('/api/preview//')
def stuff_preview(size):
text = flask.request.args.get('text')
html = flask.request.args.get('html') == '1'
r = Renderer()
r.render_text(text, 'Sans {}'.format(size), 0, -1, html)
- sio = StringIO.StringIO()
- r.surface.write_to_png(sio)
- sio.seek(0)
- return flask.send_file(sio, mimetype='image/png')
+ preview = r.surface.write_to_png()
+ return flask.Response(preview, mimetype='image/png')
DELAY = 5
-@app.route('/stuff/print//', methods=['POST'])
+@app.route('/api/print//', methods=['POST'])
def stuff_print(size):
if not healthcheck()[0]:
return 'Printer is down.'
last = app.last
- print last, time.time() - last
+ print(last, time.time() - last)
if time.time() - last < DELAY:
return 'Please wait {} more seconds before next print.'.format(int(DELAY - (time.time() - last)))
text = flask.request.args.get('text')
@@ -130,18 +125,21 @@ def stuff_print(size):
r = Renderer()
r.render_text(text, 'Sans {}'.format(size), 0, -1, html)
path = '/tmp/hslabel'
- f = open(path, 'w')
+ f = open(path, 'wb')
r.surface.write_to_png(f)
f.flush()
f.close()
time.sleep(1)
- ex = 'lpr -P DYMO_LabelWriter_450 {}'.format(path)
+ ex = 'lpr -o PageSize=w118h252 -P DYMO_LabelWriter450 {}'.format(path)
os.system(ex)
f.close()
app.last = time.time()
return 'ok'
+def main():
+ app.run(host="0.0.0.0", port=5000, debug=True)
+
if __name__ == '__main__':
- app.run(debug=True)
+ main()
diff --git a/labelmaker/templates/index.html b/labelmaker/templates/index.html
new file mode 100644
index 0000000..e8326fd
--- /dev/null
+++ b/labelmaker/templates/index.html
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+ Hackerspace Printing System For Printing
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to print this?
+
+
+
These labels cost money. Don't be an asshole, and only print stuff that is really needed.
+
+
+
+
+
+
+
+
Hackerspace Printing System For Printing Labels
+
System status: unknown
+
+
Box 'o Stuff Label For SAMLA boxes with common equipment
+
+
+
Preview
+
+
+
+
Settings
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
API
+
$ # To get a label preview
+$ curl http://label.waw.hackerspace.pl/api/preview/60/?text=foobar&html=0 | feh -
+$ # To print the label
+$ curl -d "" http://label.waw.hackerspace.pl/api/print/60/?text=foobar&html=0