"""Standalone tool to print QR codes.
Usage:
QRCodePrinter.py -p=<URL_prefix> -s <nnn> -c <nnn> -o <file>
QRCodePrinter.py -h | --help
QRCodePrinter.py --version
Options:
-p <URL_prefix>, --prefix=<URL_prefix> The URL prefix for the box number
-s <nnn>, --start=<nnn> Starting box number to use
-c <nnn>, --count=<nnn> Number of QR codes to print
-o <file>, --output=<file> Output file name
-h, --help Show this help and quit.
-v, --version Show the version of this program and quit.
"""
import logging
import logging.config
from dataclasses import dataclass, astuple, InitVar
from logging import getLogger, debug, error
from pathlib import Path
from typing import Any, Union, Optional, NamedTuple, List
from docopt import docopt
import pyqrcode
import png
import reportlab
from reportlab.pdfgen.canvas import Canvas
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.platypus import Image
from sqlalchemy import create_engine, MetaData, Table
from sqlalchemy.sql import select
import yaml # from PyYAML library
from FPIDjango.private import settings_private
__author__ = 'Travis Risner'
__project__ = "Food-Pantry-Inventory"
__creation_date__ = "05/22/2019"
# Copyright 2019 by Travis Risner - MIT License
"""
Assuming:
- letter size paper
- portrait orientation
- 1/2 inch outer margin on all sides
- all measurements in points (1 pt = 1/72 in)
- 3 labels across
- 4 labels down
- each label has 1/4 in margin on all sides
- 0, 0 of axis is in lower left corner
"""
log = None
[docs]@dataclass()
class Point:
"""
Horizontal (x) and vertical (y) coordinate.
"""
x: int
y: int
LABEL_SIZE: Point = Point(144, 144) # 2 in x 2 in
LABEL_MARGIN: Point = Point(18, 18) # 1/4 in x 1/4 in
BACKGROUND_SIZE: Point = Point(
LABEL_SIZE.x + (LABEL_MARGIN.x * 2),
LABEL_SIZE.y + (LABEL_MARGIN.y * 2))
PAGE_OFFSET: Point = Point(36, 36) # 1/2 in x 1/2 in
TITLE_ADJUSTMENT: Point = Point(+20, -9)
[docs]@dataclass
class LabelPosition:
"""
Container for measurements for one label.
All measurements are in points.
x denotes horizontal measurement
y denotes vertical
origin is in lower left corner
label is assumed to be 2 in x 2 in ( 144 pt x 144 pt)
"""
page_offset: InitVar[Point]
lower_left_offset: Point = Point(0, 0)
lower_right_offset: Point = Point(0, 0)
upper_left_offset: Point = Point(0, 0)
upper_right_offset: Point = Point(0, 0)
offset_on_page: Point = Point(0, 0)
image_start: Point = Point(0, 0)
title_start: Point = Point(0, 0)
def __post_init__(self, page_offset: Point):
"""
Adjust offsets based on offset_on_page.
:param page_offset: offset (in points) from the lower left corner
:return:
"""
self.offset_on_page = page_offset
x: int = page_offset.x
y: int = page_offset.y
offset: Point = Point(x, y)
self.lower_left_offset = offset
x = page_offset.x + BACKGROUND_SIZE.x
y = page_offset.y
offset: Point = Point(x, y)
self.lower_right_offset = offset
x = page_offset.x
y = page_offset.y + BACKGROUND_SIZE.y
offset: Point = Point(x, y)
self.upper_left_offset = offset
x = page_offset.x + BACKGROUND_SIZE.x
y = page_offset.y + BACKGROUND_SIZE.y
offset: Point = Point(x, y)
self.upper_right_offset = offset
x = self.lower_left_offset.x + LABEL_MARGIN.x
y = self.lower_left_offset.y + LABEL_MARGIN.y
self.image_start: Point = Point(x, y)
# title placement calculation
x = self.upper_left_offset.x + (LABEL_SIZE.x // 2)
y = self.upper_left_offset.y - LABEL_MARGIN.y
self.title_start: Point = Point(x, y)
return
[docs]class QRCodePrinterClass:
"""
QRCodePrinterClass - Print QR Codes
"""
def __init__(self, workdir: Path):
self.working_dir: Path = None
self.url_prefix: str = ''
self.box_start: int = 0
self.label_count: int = 0
self.output_file: str = ''
self.full_path: Path = None
self.pdf: Canvas = None
# width and height are in points (1/72 inch)
self.width: int = None
self.height: int = None
# database connection information
self.con = None
self.meta: MetaData = None
self.box: Table = None
# label locations on the page
self.label_locations: List[LabelPosition] = list()
self.compute_box_dimensions()
# set this to the last position in the list to force a new page
self.next_pos: int = len(self.label_locations)
# use the page number to control first page handling
self.page_number: int = 0
if not workdir is None and workdir.is_dir():
self.working_dir = workdir
return
[docs] def run_QRPrt(self, parameters: dict):
"""
Top method for running Run the QR code printer..
:param parameters: dictionary of command line arguments
:return:
"""
parm_dict = parameters
self.url_prefix: str = parm_dict['--prefix'].strip('\'"')
self.box_start: int = int(parm_dict['--start'])
self.label_count: int = int(parm_dict['--count'])
self.output_file: str = parm_dict['--output']
if (not isinstance(self.box_start, int)) or \
self.box_start <= 0:
raise ValueError('Box start must be a positive integer')
if (not isinstance(self.label_count, int)) or \
self.label_count <= 0:
raise ValueError('Label count must be a positive integer')
full_path = self.working_dir / self.output_file
if full_path.exists():
raise ValueError('File already exists')
else:
self.full_path = full_path
debug(
f'Parameters validated: pfx: {self.url_prefix}, '
f'start: {self.box_start}, '
f'count: {self.label_count}, '
f'file: {self.output_file}'
)
self.connect_to_generate_labels()
return
[docs] def connect_to_generate_labels(self):
"""
Connect to the database and generate labels.
:return:
"""
# establish access to the database
self.con, self.meta = self.connect(
user=settings_private.DB_USER,
password=settings_private.DB_PSWD,
db=settings_private.DB_NAME,
host=settings_private.DB_HOST,
port=settings_private.DB_PORT
)
# establish access to the box table
self.box = Table(
'fpiweb_box',
self.meta,
autoload=True,
autoload_with=self.con)
self.generate_label_pdf()
# self.con.close()
return
[docs] def connect(self, user, password, db, host='localhost', port=5432):
"""
Establish a connection to the desired PostgreSQL database.
:param user:
:param password:
:param db:
:param host:
:param port:
:return:
"""
# We connect with the help of the PostgreSQL URL
# postgresql://federer:grandestslam@localhost:5432/tennis
url = f'postgresql://{user}:{password}@{host}:{port}/{db}'
# The return value of create_engine() is our connection object
con = create_engine(url, client_encoding='utf8')
# We then bind the connection to MetaData()
meta = MetaData(bind=con)
return con, meta
[docs] def generate_label_pdf(self):
"""
Generate the pdf file with the requested labels in it.
:return:
"""
self.initialize_pdf_file()
self.fill_pdf_pages()
self.finalize_pdf_file()
return
[docs] def initialize_pdf_file(self):
"""
Setup the pdf to receive labels.
:return:
"""
self.pdf = Canvas(str(self.full_path), pagesize=letter)
self.width, self.height = letter
return
[docs] def compute_box_dimensions(self):
"""
Compute the dimensions and bounding boxes for each label on the page.
:return:
"""
vertical_start = (BACKGROUND_SIZE.y * 3) + PAGE_OFFSET.y
horizontal_stop = (BACKGROUND_SIZE.x * 3) + PAGE_OFFSET.x - 1
for vertical_position in range(vertical_start, -1,
-BACKGROUND_SIZE.y):
for horizontal_position in range(PAGE_OFFSET.x,
horizontal_stop,
BACKGROUND_SIZE.x):
new_label = LabelPosition(Point(horizontal_position,
vertical_position))
self.label_locations.append(new_label)
return
[docs] def fill_pdf_pages(self):
"""
Fill one or more pages with labels.
:return:
"""
# # draw lines around the boxes that will be filled with labels
# self.draw_boxes_on_page()
# # self.pdf.setFillColorRGB(1, 0, 1)
# # self.pdf.rect(2*inch, 2*inch, 2*inch, 2*inch, fill=1)
for label_file, label_name in self.get_next_qr_img():
debug(f'Got {label_file}')
if self.next_pos >= len(self.label_locations) - 1:
self. finish_page()
self.next_pos = 0
else:
self.next_pos += 1
self.draw_bounding_box(self.next_pos)
self.place_label(label_file, label_name, self.next_pos)
self.finish_page()
return
[docs] def place_label(self, file_name: str, label_name: str, pos: int):
"""
Place the label in the appropriate location on the page.
:param file_name:
:param label_name:
:param pos:
:return:
"""
box_info = self.label_locations[pos]
# place image on page
im = Image(file_name, LABEL_SIZE.x, LABEL_SIZE.y)
im.drawOn(self.pdf, box_info.image_start.x, box_info.image_start.y)
# place title above image
self.pdf.setFont('Helvetica-Bold', 12)
self.pdf.drawCentredString(
box_info.title_start.x + TITLE_ADJUSTMENT.x,
box_info.title_start.y + TITLE_ADJUSTMENT.y,
label_name
)
return
[docs] def finish_page(self):
"""
Finish off the prefious page before starting a new one
"""
if self.page_number > 0:
self.pdf.showPage()
self.page_number += 1
return
[docs] def draw_bounding_box(self, label_pos: int):
"""
Draw a bounding box around the specified label.
:param label_pos: position in the labels locations list.
:return:
"""
box_info = self.label_locations[label_pos]
self.pdf.line(box_info.upper_left_offset.x,
box_info.upper_left_offset.y,
box_info.upper_right_offset.x,
box_info.upper_right_offset.y)
self.pdf.line(box_info.upper_right_offset.x,
box_info.upper_right_offset.y,
box_info.lower_right_offset.x,
box_info.lower_right_offset.y)
self.pdf.line(box_info.lower_right_offset.x,
box_info.lower_right_offset.y,
box_info.lower_left_offset.x,
box_info.lower_left_offset.y)
self.pdf.line(box_info.lower_left_offset.x,
box_info.lower_left_offset.y,
box_info.upper_left_offset.x,
box_info.upper_left_offset.y)
return
[docs] def get_next_qr_img(self) -> (str, str):
"""
Build the QR image for the next box label.
:return: a QR code image ready to print
"""
for url, label in self.get_next_box_url():
label_file_name = f'{label}.png'
qr = pyqrcode.create(url)
qr.png(label_file_name, scale=5)
yield label_file_name, label
return
[docs] def get_next_box_url(self) -> (str, str):
"""
Build the URL for the next box.
:return:
"""
for label, box_number in self.get_next_box_number():
debug(f'Got {label}, {box_number}')
url = f"{self.url_prefix}{box_number:05}"
yield url, label
return
[docs] def get_next_box_number(self) -> (str, int):
"""
Search for the next box number to go on a label.
:return:
"""
next_box_number = self.box_start
available_count = 0
while available_count < self.label_count:
box_label = f'BOX{next_box_number:05}'
debug(f'Attempting to get {box_label}')
sel_box_stm = select([self.box]).where(
self.box.c.box_number == box_label)
# box_found = exists(sel_box_stm)
# exist_stm = exists().where(self.box.c.box_number == box_label)
result = self.con.execute(sel_box_stm)
debug(f'Search result: {result.rowcount}')
box = result.fetchone()
if not box:
# found a hole in the numbers
available_count += 1
debug(f'{box_label} not found - using for label')
yield (box_label, next_box_number)
else:
result.close()
next_box_number += 1
return
[docs] def finalize_pdf_file(self):
"""
All pages have been generated so flush all buffers and close.
:return:
"""
self.pdf.save()
return
[docs]class Main:
"""
Main class to start things rolling.
"""
def __init__(self):
"""
Get things started.
"""
self.QRCodePtr: QRCodePrinterClass = None
self.working_dir: Path = None
return
[docs] def run_QRCodePtr(self, arguments: dict):
"""
Prepare to run Run the QR code printer..
:return:
"""
self.QRCodePtr = QRCodePrinterClass(workdir=self.working_dir)
debug('Starting up QRCodePtr')
self.QRCodePtr.run_QRPrt(arguments)
return
[docs] def start_logging(self, work_dir: Path, debug_name: str):
"""
Establish the logging for all the other scripts.
:param work_dir:
:param debug_name:
:return: (nothing)
"""
# Set flag that no logging has been established
logging_started = False
# find our working directory and possible logging input file
_workdir = work_dir
_logfilename = debug_name
# obtain the full path to the log information
_debugConfig = _workdir / _logfilename
# verify that the file exists before trying to open it
if Path.exists(_debugConfig):
try:
# get the logging params from yaml file and instantiate a log
with open(_logfilename, 'r') as _logdictfd:
_logdict = yaml.load(_logdictfd, Loader=yaml.SafeLoader)
logging.config.dictConfig(_logdict)
logging_started = True
except Exception as xcp:
print(f'The file {_debugConfig} exists, but does not contain '
f'appropriate logging directives.')
raise ValueError('Invalid logging directives.')
else:
print(f'Logging directives file {_debugConfig} either not '
f'specified or not found')
if not logging_started:
# set up minimal logging
_logfilename = 'debuginfo.txt'
_debugConfig = _workdir / _logfilename
logging.basicConfig(filename='debuginfo.txt', level=logging.INFO,
filemode='w')
print(f'Minimal logging established to {_debugConfig}')
# start logging
global log
log = logging.getLogger(__name__)
logging.info(f'Logging started: working directory is {_workdir}')
# sset confirmed working directory to pass on to target class
self.working_dir = _workdir
return
if __name__ == "__main__":
arguments = docopt(__doc__, version='QRCodePrinter 1.0')
workdir = Path.cwd()
debug_file_name = 'debug_info.yaml'
main = Main()
main.start_logging(workdir, debug_file_name)
debug('Parameters as interpreted by docopt')
for arg in arguments:
debug(f'arg key: {arg}, value: {arguments[arg]}')
main.run_QRCodePtr(arguments)
# EOF