Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM python:3-alpine

COPY . /app
WORKDIR /app

RUN apk add fontconfig \
git \
ttf-dejavu \
ttf-liberation \
ttf-droid \
font-terminus \
font-inconsolata \
font-dejavu \
font-noto \
poppler-utils && \
fc-cache -f && \
pip3 install -r requirements.txt

EXPOSE 8013
ENTRYPOINT [ "python3", "run.py" ]
91 changes: 64 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
## brother\_ql\_web
## brother_ql_web

This is a web service to print labels on Brother QL label printers.

You need Python 3 for this software to work.
You need Python 3 or Docker for this software to work.

![Screenshot](./screenshots/Label-Designer_Desktop.png)

The web interface is [responsive](https://en.wikipedia.org/wiki/Responsive_web_design).
There's also a screenshot showing [how it looks on a smartphone](./screenshots/Label-Designer_Phone.png)

### Additional Features
* Print text as QR Code
* Add text to QR Code
* Change size of QR Code
* Upload files to print
* .pdf, .png and .jpg files
* automatically convertion to black/white image
* Change print color for black/white/red labels
* Print lables multiple times
* Cut every label
* Cut only after the last label
* Migrated GUI to Bootstrap 4
* Make preview for round labels.. round

- Print text as QR Code
- Add text to QR Code
- Change size of QR Code
- Upload files to print
- .pdf, .png and .jpg files
- automatically convertion to black/white image
- Change print color for black/white/red labels
- Print lables multiple times
- Cut every label
- Cut only after the last label
- Migrated GUI to Bootstrap 4
- Make preview for round labels.. round

### Installation

Expand All @@ -42,20 +43,25 @@ Build the venv and install the requirements:
source /opt/brother_ql_web/.venv/bin/activate
pip install -r requirements.txt

### Configuration file
#### Configuration file

Create a directory called 'instance', a file called 'application.py' and adjust the values to match your needs.

```bash
mkdir /opt/brother_ql_web/instance
touch /opt/brother_ql_web/instance/application.py
```

E.g.
"""
User specific application settings
"""
import logging
PRINTER_MODEL = 'QL-820NWB'
PRINTER_PRINTER = 'tcp://192.168.1.33:9100'

```python
"""
User specific application settings
"""
import logging
PRINTER_MODEL = 'QL-820NWB'
PRINTER_PRINTER = 'tcp://192.168.1.33:9100'
```

### Startup

Expand All @@ -70,6 +76,37 @@ Copy service file, reload system, enable and start the service
systemctl enable brother_ql_web
systemctl start brother_ql_web

### Run via Docker

To build the image:

```bash
git clone https://github.com/tbnobody/brother_ql_web.git
cd brother_ql_web
docker buildx build -t brother-ql-web .

# alternatively, if buildx is not available
docker build -t brother-ql-web .
```

You can then start your newly build image with `docker run`.
You have to pass your printer model as `--model` argument. At the end of the arguments you have to add your device socket (linux kernel backend), USB identifier (pyusb backend) or network address (TCP).
Please note you might have to pass your device to the container via the `--device` flag.

Example command to start the application, connecting to a QL-800 on `/dev/usb/lp0`, setting label size to 62mm:

```bash
docker run -d \
--restart=always \
--name=brother-ql-web \
-p 8013:8013 \
--device=/dev/usb/lp0 \
brother-ql-web:latest \
--default-label-size 62 \
--model QL-800 \
file:///dev/usb/lp0
```

### Usage

Once it's running, access the web interface by opening the page with your browser.
Expand All @@ -78,16 +115,16 @@ You will then be forwarded by default to the interactive web gui located at `/la

All in all, the web server offers:

* a Web GUI allowing you to print your labels at `/labeldesigner`,
* an API at `/api/print/text?text=Your_Text&font_size=100&font_family=Minion%20Pro%20(%20Semibold%20)`
to print a label containing 'Your Text' with the specified font properties.
- a Web GUI allowing you to print your labels at `/labeldesigner`,
- an API at `/api/print/text?text=Your_Text&font_size=100&font_family=Minion%20Pro%20(%20Semibold%20)`
to print a label containing 'Your Text' with the specified font properties.

### License

This software is published under the terms of the GPLv3, see the LICENSE file in the repository.

Parts of this package are redistributed software products from 3rd parties. They are subject to different licenses:

* [Bootstrap](https://github.com/twbs/bootstrap), MIT License
* [Font Awesome](https://github.com/FortAwesome/Font-Awesome), CC BY 4.0 License
* [jQuery](https://github.com/jquery/jquery), MIT License
- [Bootstrap](https://github.com/twbs/bootstrap), MIT License
- [Font Awesome](https://github.com/FortAwesome/Font-Awesome), CC BY 4.0 License
- [jQuery](https://github.com/jquery/jquery), MIT License
39 changes: 39 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@

import sys
import random
import argparse

from flask import Flask
from flask_bootstrap import Bootstrap

from brother_ql.devicedependent import models

from . import fonts
from config import Config

Expand Down Expand Up @@ -46,6 +49,9 @@ def main(app):

FONTS = fonts.Fonts()
FONTS.scan_global_fonts()

parse_args(app)

if app.config['FONT_FOLDER']:
FONTS.scan_fonts_folder(app.config['FONT_FOLDER'])

Expand All @@ -67,3 +73,36 @@ def main(app):
app.config['LABEL_DEFAULT_FONT_STYLE'] = style
app.logger.warn(
'The default font is now set to: {} ({})\n'.format(family, style))


def parse_args(app):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--default-label-size', default=False,
help='Label size inserted in your printer. Defaults to 62.')
parser.add_argument('--default-orientation', default=False, choices=('standard', 'rotated'),
help='Label orientation, defaults to "standard". To turn your text by 90°, state "rotated".')
parser.add_argument('--model', default=False, choices=models,
help='The model of your printer (default: QL-500)')
parser.add_argument('printer', nargs='?', default=False,
help='String descriptor for the printer to use (like tcp://192.168.0.23:9100 or file:///dev/usb/lp0)')
args = parser.parse_args()

if args.printer:
app.config.update(
PRINTER_PRINTER=args.printer
)

if args.model:
app.config.update(
PRINTER_MODEL=args.model
)

if args.default_label_size:
app.config.update(
LABEL_DEFAULT_SIZE=args.default_label_size
)

if args.default_orientation:
app.config.update(
LABEL_DEFAULT_ORIENTATION=args.default_orientation
)
4 changes: 3 additions & 1 deletion app/labeldesigner/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class LabelContent(Enum):
TEXT_QRCODE = auto()
IMAGE_BW = auto()
IMAGE_GRAYSCALE = auto()
IMAGE_RED_BLACK = auto()
IMAGE_COLORED = auto()


class LabelOrientation(Enum):
Expand Down Expand Up @@ -116,7 +118,7 @@ def label_type(self, value):
def generate(self):
if self._label_content in (LabelContent.QRCODE_ONLY, LabelContent.TEXT_QRCODE):
img = self._generate_qr()
elif self._label_content in (LabelContent.IMAGE_BW, LabelContent.IMAGE_GRAYSCALE):
elif self._label_content in (LabelContent.IMAGE_BW, LabelContent.IMAGE_GRAYSCALE, LabelContent.IMAGE_RED_BLACK, LabelContent.IMAGE_COLORED):
img = self._image
else:
img = None
Expand Down
6 changes: 3 additions & 3 deletions app/labeldesigner/printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,10 @@ def process_queue(self):

img = queue_entry['label'].generate()

if queue_entry['label'].label_content == LabelContent.IMAGE_GRAYSCALE:
dither = True
else:
if queue_entry['label'].label_content == LabelContent.IMAGE_BW:
dither = False
else:
dither = True

create_label(
qlr,
Expand Down
10 changes: 9 additions & 1 deletion app/labeldesigner/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from brother_ql.devicedependent import ENDLESS_LABEL, DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL

from . import bp
from app.utils import convert_image_to_bw, convert_image_to_grayscale, pdffile_to_image, imgfile_to_image, image_to_png_bytes
from app.utils import convert_image_to_bw, convert_image_to_grayscale, convert_image_to_red_and_black, pdffile_to_image, imgfile_to_image, image_to_png_bytes
from app import FONTS

from .label import SimpleLabel, LabelContent, LabelOrientation, LabelType
Expand Down Expand Up @@ -171,6 +171,10 @@ def get_uploaded_image(image):
image = imgfile_to_image(image)
if context['image_mode'] == 'grayscale':
return convert_image_to_grayscale(image)
elif context['image_mode'] == 'red_and_black':
return convert_image_to_red_and_black(image)
elif context['image_mode'] == 'colored':
return image
else:
return convert_image_to_bw(image, context['image_bw_threshold'])
elif ext.lower() in ('.pdf'):
Expand All @@ -192,6 +196,10 @@ def get_uploaded_image(image):
label_content = LabelContent.TEXT_QRCODE
elif context['image_mode'] == 'grayscale':
label_content = LabelContent.IMAGE_GRAYSCALE
elif context['image_mode'] == 'red_black':
label_content = LabelContent.IMAGE_RED_BLACK
elif context['image_mode'] == 'colored':
label_content = LabelContent.IMAGE_COLORED
else:
label_content = LabelContent.IMAGE_BW

Expand Down
30 changes: 20 additions & 10 deletions app/labeldesigner/templates/labeldesigner.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,26 @@
<div id="collapse5" class="collapse" aria-labelledby="heading5" data-parent="#accordion">
<div class="card-body">
<label for="imageMode" class="control-label input-group" style="margin-top: 10px; margin-bottom: 0">Image mode:</label>
<div class="btn-group btn-group-toggle btn-block" data-toggle="buttons">
<label class="btn btn-secondary {% if default_image_mode == 'black_and_white' %}active{% endif %}" id="image_mode_bw">
<input type="radio" name="imageMode" onchange="preview()" value="black_and_white" aria-label="Black & White" {% if default_image_mode == 'black_and_white' %}checked{% endif %}>
<span class="fas fa-ruler-horizontal" aria-hidden="true"> Black & White
</label>
<label class="btn btn-secondary {% if default_image_mode == 'grayscale' %}active{% endif %}" id="image_mode_grayscale">
<input type="radio" name="imageMode" onchange="preview()" value="grayscale" aria-label="Grayscale" {% if default_image_mode == 'grayscale' %}checked{% endif %}>
<span class="fas fa-ruler-vertical" aria-hidden="true"> Grayscale
</label>
</div>

<label class="btn btn-secondary {% if default_image_mode == 'black_and_white' %}active{% endif %}" id="image_mode_bw">
<input type="radio" name="imageMode" onchange="preview()" value="black_and_white" aria-label="Black & White" {% if default_image_mode == 'black_and_white' %}checked{% endif %}>
<span class="fas fa-ruler-horizontal" aria-hidden="true"> Black & White
</label>
<label class="btn btn-secondary {% if default_image_mode == 'grayscale' %}active{% endif %}" id="image_mode_grayscale">
<input type="radio" name="imageMode" onchange="preview()" value="grayscale" aria-label="Grayscale" {% if default_image_mode == 'grayscale' %}checked{% endif %}>
<span class="fas fa-ruler-vertical" aria-hidden="true"> Grayscale
</label>
{% if red_support %}
<label class="btn btn-secondary {% if default_image_mode == 'colored' %}active{% endif %}" id="image_mode_colored">
<input type="radio" name="imageMode" onchange="preview()" value="colored" aria-label="Colored" {% if default_image_mode == 'colored' %}checked{% endif %}>
<span class="fas fa-ruler-horizontal" aria-hidden="true"> Colored
</label>
<label class="btn btn-secondary {% if default_image_mode == 'red_and_black' %}active{% endif %}" id="image_mode_red_and_black">
<input type="radio" name="imageMode" onchange="preview()" value="red_and_black" aria-label="Red & Black" {% if default_image_mode == 'red_and_black' %}checked{% endif %}>
<span class="fas fa-ruler-vertical" aria-hidden="true"> Red & Black
</label>
{% endif %}


<label for="imageBwThreshold" style="margin-top: 10px; margin-bottom: 0">Black & White threshold:</label>
<input id="imageBwThreshold" class="form-control" type="number" min="1" max="255" value="{{default_bw_threshold}}" onChange="preview()">
Expand Down
4 changes: 4 additions & 0 deletions app/labeldesigner/templates/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,13 @@ function preview() {
if ($('#labelSize option:selected').val().includes('red')) {
$('#print_color_black').removeClass('disabled');
$('#print_color_red').removeClass('disabled');
$('#image_mode_red_and_black').removeClass('disabled');
$('#image_mode_colored').removeClass('disabled');
} else {
$('#print_color_black').addClass('disabled').prop('active', true);
$('#print_color_red').addClass('disabled');
$('#image_mode_red_and_black').addClass('disabled');
$('#image_mode_colored').addClass('disabled');
}
{% endif %}

Expand Down
4 changes: 4 additions & 0 deletions app/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-

from PIL import Image
from PIL.ImageOps import colorize
from io import BytesIO
from pdf2image import convert_from_bytes

Expand All @@ -13,6 +14,9 @@ def convert_image_to_grayscale(image):
fn = lambda x : 255 if x > threshold else 0
return image.convert('L') # convert to greyscale

def convert_image_to_red_and_black(image):
return colorize(image.convert('L'), black='black', white='white', mid='red')


def imgfile_to_image(file):
s = BytesIO()
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
brother_ql
brother_ql-inventree
Flask
flask_bootstrap4
qrcode
Expand Down