Initial commit
This commit is contained in:
commit
ebc088198f
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
*.pyc
|
||||||
|
*.egg-info
|
||||||
|
build
|
||||||
|
dist
|
||||||
1
MANIFEST.in
Normal file
1
MANIFEST.in
Normal file
@ -0,0 +1 @@
|
|||||||
|
recursive-include pylabel/templates *
|
||||||
14
README.md
Normal file
14
README.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# PyLabel
|
||||||
|
|
||||||
|
Generate labels for printing using HTML rendering.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
* Customizable for most label sheet layouts.
|
||||||
|
* Custom data for each label
|
||||||
|
* Uses HTML templates for easy customization
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
* Switch to profiles for command line usage
|
||||||
|
* Switch to
|
||||||
113
pylabel/__init__.py
Normal file
113
pylabel/__init__.py
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import os.path
|
||||||
|
import math
|
||||||
|
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), 'templates')
|
||||||
|
|
||||||
|
# load the default style from the template
|
||||||
|
with open(os.path.join(TEMPLATE_DIR, 'layout.css'), 'r') as f:
|
||||||
|
LAYOUT_STYLE = f.read()
|
||||||
|
|
||||||
|
PAPER_SIZES = {
|
||||||
|
'A4': (210, 297)
|
||||||
|
}
|
||||||
|
|
||||||
|
class LabelSheet(object):
|
||||||
|
|
||||||
|
def __init__(self, profile):
|
||||||
|
|
||||||
|
logger.debug(profile)
|
||||||
|
|
||||||
|
self.size = profile['size']
|
||||||
|
logger.debug(self.size)
|
||||||
|
|
||||||
|
# get the pagesize and rotate if landscape
|
||||||
|
pagesize = profile.get('pagesize', 'a4')
|
||||||
|
if not isinstance(pagesize, (list, tuple)):
|
||||||
|
pagesize = PAPER_SIZES[pagesize.upper()]
|
||||||
|
if profile.get('landscape'):
|
||||||
|
pagesize = (pagesize[1], pagesize[0])
|
||||||
|
logger.debug("Pagesize: %r", pagesize)
|
||||||
|
self.pagesize = pagesize
|
||||||
|
|
||||||
|
# get the margins
|
||||||
|
margins = profile['margins']
|
||||||
|
if len(margins) == 2:
|
||||||
|
margins += margins
|
||||||
|
logger.debug("Margins: %r", margins)
|
||||||
|
self.margins = margins
|
||||||
|
|
||||||
|
# Calculate the printable area
|
||||||
|
self.printarea = (
|
||||||
|
pagesize[0]-margins[0]-margins[2],
|
||||||
|
pagesize[1]-margins[1]-margins[3]
|
||||||
|
)
|
||||||
|
logger.debug("Print area: %r", self.printarea)
|
||||||
|
|
||||||
|
# Calculate rows and cols if not specified
|
||||||
|
layout = profile.get('layout')
|
||||||
|
if not layout:
|
||||||
|
layout = (
|
||||||
|
int(self.printarea[0] / self.size[0]),
|
||||||
|
int(self.printarea[1] / self.size[1])
|
||||||
|
)
|
||||||
|
logger.debug("Layout: %r", layout)
|
||||||
|
self.layout = layout
|
||||||
|
|
||||||
|
# Calculate gaps between labels
|
||||||
|
self.gaps = (
|
||||||
|
round((self.printarea[0] - layout[0]*self.size[0]) / (layout[0]-1), 1),
|
||||||
|
round((self.printarea[1] - layout[1]*self.size[1]) / (layout[1]-1), 1)
|
||||||
|
)
|
||||||
|
logger.debug("Gaps: %r", self.gaps)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stylesheet(self):
|
||||||
|
return LAYOUT_STYLE + """
|
||||||
|
|
||||||
|
SECTION.page {{
|
||||||
|
width: {o.pagesize[0]:0.1f}mm;
|
||||||
|
height: {o.pagesize[1]:0.1f}mm;
|
||||||
|
padding-top: {o.margins[1]:0.1f}mm;
|
||||||
|
}}
|
||||||
|
|
||||||
|
DIV.labelarea {{
|
||||||
|
margin-left: {o.margins[0]:0.1f}mm;
|
||||||
|
}}
|
||||||
|
|
||||||
|
DIV.label {{
|
||||||
|
width: {o.size[0]:0.1f}mm;
|
||||||
|
height: {o.size[1]:0.1f}mm;
|
||||||
|
margin-right: {o.gaps[0]:0.1f}mm;
|
||||||
|
margin-bottom: {o.gaps[1]:0.1f}mm;
|
||||||
|
}}
|
||||||
|
""".format(o=self)
|
||||||
|
|
||||||
|
def render_page(self, template, data, skip=0):
|
||||||
|
|
||||||
|
label_template = f'<div class="label"><div class="content">{template}</div></div>'
|
||||||
|
|
||||||
|
blank_labels = "".join([ '<div class="label"><div class="content"> </div></div>' for x in range(skip) ])
|
||||||
|
|
||||||
|
labels = "".join([ label_template.format(data=x) for x in data ])
|
||||||
|
|
||||||
|
|
||||||
|
return """
|
||||||
|
<section class="page">
|
||||||
|
<div class="labelarea">{labels}</div>
|
||||||
|
</section>
|
||||||
|
""".format(labels=blank_labels+labels)
|
||||||
|
|
||||||
|
def render_html(self, template, data, skip=0, extra_style=""):
|
||||||
|
|
||||||
|
page = self.render_page(template, data, skip)
|
||||||
|
|
||||||
|
with open(os.path.join(TEMPLATE_DIR, 'default.html'), 'r') as f:
|
||||||
|
html = f.read()
|
||||||
|
|
||||||
|
return html.format(style=self.stylesheet + extra_style, label_sheet=page)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"<LabelSheet {self.size}, {self.pagesize}>"
|
||||||
57
pylabel/__main__.py
Normal file
57
pylabel/__main__.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
from . import LabelSheet
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Generate a page of labels")
|
||||||
|
|
||||||
|
parser.add_argument("profile", help="Profile to load")
|
||||||
|
|
||||||
|
parser.add_argument('--style', '-s', type=argparse.FileType('r'), help='Additional stylesheet')
|
||||||
|
parser.add_argument('--skip', '-n', type=int, default=0, help="Skip first n labels")
|
||||||
|
parser.add_argument('--template', '-t', type=argparse.FileType('r'), help='Template for label')
|
||||||
|
parser.add_argument('--json', '-j', action='store_true', help='Parse lines as json')
|
||||||
|
|
||||||
|
parser.add_argument('--logging', '-l', default="info", help="Logging level")
|
||||||
|
options = parser.parse_args()
|
||||||
|
|
||||||
|
logging.basicConfig(level=getattr(logging, options.logging.upper(), logging.INFO))
|
||||||
|
|
||||||
|
logger.debug(options)
|
||||||
|
|
||||||
|
try:
|
||||||
|
module, profile = options.profile.rsplit('.', 1)
|
||||||
|
m = importlib.import_module(module)
|
||||||
|
p = getattr(m, profile)
|
||||||
|
except ValueError:
|
||||||
|
raise RuntimeError("Expected profile in the form module.profile")
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
raise RuntimeError(f"Unable to find profile module {module}")
|
||||||
|
except AttributeError:
|
||||||
|
raise RuntimeError(f"Unable to file profile {profile} in module {module}")
|
||||||
|
|
||||||
|
sheet = LabelSheet(p)
|
||||||
|
|
||||||
|
extra_style=""
|
||||||
|
if options.style is not None:
|
||||||
|
extra_style = options.style.read()
|
||||||
|
options.style.close()
|
||||||
|
|
||||||
|
template = '<b>{data[text]}</b>'
|
||||||
|
if options.template is not None:
|
||||||
|
template = options.template.read()
|
||||||
|
options.template.close()
|
||||||
|
|
||||||
|
lines = [ l.strip() for l in sys.stdin.readlines() if l != '\n' ]
|
||||||
|
|
||||||
|
if options.json:
|
||||||
|
import json
|
||||||
|
data = [ json.loads(line) for line in lines ]
|
||||||
|
else:
|
||||||
|
data = [ {'text': line} for line in lines ]
|
||||||
|
|
||||||
|
sys.stdout.write(sheet.render_html(template, data, options.skip, extra_style))
|
||||||
7
pylabel/avery.py
Normal file
7
pylabel/avery.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
L7157 = {
|
||||||
|
"size": (64, 24.3),
|
||||||
|
"layout": (3, 11),
|
||||||
|
"papersize": "a4",
|
||||||
|
"margins": (6, 14.5),
|
||||||
|
}
|
||||||
11
pylabel/templates/default.html
Normal file
11
pylabel/templates/default.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Label Sheet</title>
|
||||||
|
<style>
|
||||||
|
{style}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{label_sheet}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
33
pylabel/templates/layout.css
Normal file
33
pylabel/templates/layout.css
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
@media print {
|
||||||
|
@page { margin: 0; }
|
||||||
|
|
||||||
|
SECTION.page {
|
||||||
|
page-break-after: always;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-print-color-adjust: exact !important; /* Chrome, Safari */
|
||||||
|
color-adjust: exact !important; /*Firefox*/
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen {
|
||||||
|
|
||||||
|
SECTION.page {
|
||||||
|
background-color: #EEE;
|
||||||
|
margin: 20pt auto 50pt;
|
||||||
|
box-shadow: 10px 10px 5px grey;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DIV.label {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #FFF;
|
||||||
|
border-radius: 2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
DIV.content {
|
||||||
|
margin: 2mm; /* Minimum to be safe */
|
||||||
|
}
|
||||||
21
setup.cfg
Normal file
21
setup.cfg
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
[metadata]
|
||||||
|
name = pylabel-tf198
|
||||||
|
version = 0.0.1
|
||||||
|
author = Tris Forster
|
||||||
|
author_email = tris@shoddynet.org
|
||||||
|
description = Generates label sheets for printing
|
||||||
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
|
url = https://github.com/tf198/pylabel
|
||||||
|
project_urls =
|
||||||
|
Bug Tracker = https://github.com/tf198/pylabel/issues
|
||||||
|
classifiers =
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
License :: OSI Approved :: MIT License
|
||||||
|
Operating System :: OS Independent
|
||||||
|
|
||||||
|
[options]
|
||||||
|
packages = find:
|
||||||
|
include_package_data = True
|
||||||
|
python_requires = >=3.6
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user