12 Commits

9 changed files with 212 additions and 187 deletions

3
.gitignore vendored
View File

@@ -1 +1,4 @@
*~
partdb-labeler.bat
partdb_labeler.egg-info
build

View File

@@ -25,4 +25,19 @@ printer via USB.
Usage
-----
```python -m partdb-labeler -h``` will show you the available options.
```partdb_labeler -h``` will show you the available options.
For convenience, you might also consider adding a short shell script somewhere in your PATH that will call the Python module
with your server configuration. I use this (the API key is a read-only key I've publicized elsewhere):
```
#!/usr/bin/env bash
partdb_labeler -p https://partdb.alfter.us -k tcp_673fc81f0b7837ca4c029fbd6536b27742eb8b742eba27bf547c8136dc6a84f8 $*
```
or the same, as a batch file on Windows:
```
@echo off
partdb_labeler -p https://partdb.alfter.us -k tcp_673fc81f0b7837ca4c029fbd6536b27742eb8b742eba27bf547c8136dc6a84f8 %*
```

View File

Binary file not shown.

View File

Binary file not shown.

View File

@@ -1,174 +0,0 @@
#!/usr/bin/env python
import requests
from zebra import Zebra
import qrcode
from math import ceil, floor
from PIL import Image
import textwrap
import argparse
parser=argparse.ArgumentParser()
parser.add_argument("id", help="part ID", type=int)
parser.add_argument("-x", help="label width, in inches (default: 2\")", type=float)
parser.add_argument("-y", help="label height, in inches (default: 1\")", type=float)
parser.add_argument("-g", help="label gap, in inches (default: 0.118\")", type=float)
parser.add_argument("-q", help="send to selected print queue instead of stdout")
parser.add_argument("-p", help="PartDB base URL (default: https://partdb.alfter.us)")
parser.add_argument("-k", help="PartDB API key (default: anonymous key for partdb.alfter.us)")
parser.add_argument("-r", help="printer resolution (203 or 300; default: 203 dpi)", type=int)
args=parser.parse_args()
id=args.id
if args.x==None:
label_width=2
else:
label_width=args.x
if args.y==None:
label_height=1
else:
label_height=args.y
if args.g==None:
label_gap=0.118
else:
label_gap=args.g
if args.q==None:
queue="zebra_python_unittest"
else:
queue=args.q
if args.p==None:
base_url="https://partdb.alfter.us"
else:
base_url=args.p
if args.k==None:
api_key="tcp_673fc81f0b7837ca4c029fbd6536b27742eb8b742eba27bf547c8136dc6a84f8"
else:
api_key=args.k
if args.r==None:
res=203
else:
res=args.r
if res!=203 and res!=300:
raise ValueError("valid resolution options are 203 and 300")
# width and height for built-in monospace fonts (includes whitespace)
font_metrics={}
font_metrics[1]=(10,14)
font_metrics[2]=(12,18)
font_metrics[3]=(14,22)
font_metrics[4]=(16,26)
font_metrics[5]=(34,50)
# make substitutions for characters not in CP437
def subst(s):
repl={}
repl["®"]="(R)"
repl["©"]="(C)"
repl[""]="Ω" # U+2126 -> U+03A9, which is in CP437
repl["±"]="+/-"
out=""
for i in s:
try:
out=out+repl[i]
except:
out=out+i
return out
# filter out characters not in selected codepage
# (printer uses CP437)
def filter(s, cp):
out=""
for i in s:
try:
i.encode(cp)
out=out+i
except:
pass
return out
# handle escape characters in strings to be printed
def esc(s):
out=""
for i in s:
if i=="\"":
out=out+"\\\""
elif i=="\\":
out=out+"\\\\"
else:
out=out+i
return out
# render a line of text at coordinates
# return coordinates of next line
def textline(s, loc, fontnum):
z.output(f"A{loc[0]},{loc[1]},0,{fontnum},1,1,N,\"{esc(filter(subst(s), "cp437"))}\"\n")
return (loc[0], loc[1]+font_metrics[fontnum][1])
# wrap text in a bounding box at coordinates
# return coordinates of next line and any unused text
def textbox(s, loc, bbox, fontnum):
wrapped=textwrap.wrap(filter(subst(s), "cp437"), width=floor(bbox[0]/font_metrics[fontnum][0]))
line=0
while line*font_metrics[fontnum][1]<bbox[1] and line<len(wrapped):
loc=textline(wrapped[line], loc, fontnum)
line=line+1
return loc, " ".join(wrapped[line:])
# render a QR code at coordinates
# return size (single value, since QR codes are square)
def qr(s, loc, mul, brdr):
qr = qrcode.QRCode(
box_size=mul,
border=brdr,
)
qr.add_data(s)
qr.make(fit=True)
img = qr.make_image().copy()
padded=Image.new(mode="1", size=(8*ceil(img.width/8),img.height), color="white")
padded.paste(im=img, box=(0,0,img.width,img.height))
z.output(f"GW10,10,{ceil(padded.width/8)},{padded.height},{padded.tobytes().decode("cp437")}\n")
return img.height
# look up the part
url=f"{base_url}/api/parts/{id}"
headers={}
headers["Accept"]="application/json"
if api_key!=None:
headers["Authorization"]=f"Bearer {api_key}"
part=requests.get(url, headers=headers).json()
# render a label for it
label_width=floor(label_width*res)
label_height=floor(label_height*res)
label_gap=floor(label_gap*res)
z=Zebra(queue)
z.output(f"q{label_width}\n")
if (args.y!=None and args.g!=None):
z.output(f"Q{label_height},{label_gap}\n")
z.output("N\n")
if res==300:
qr_size=qr(f"{base_url}/en/part/{id}", (10, 10), 6, 3)
else:
qr_size=qr(f"{base_url}/en/part/{id}", (10, 10), 4, 2)
loc=(15+qr_size, 20)
loc=textline(part["ipn"], loc, 5)
loc, excess=textbox(f"{part["manufacturer"]["name"]} {part["manufacturer_product_number"]}", loc, (label_width-loc[0]-10, label_height-loc[1]-10), 2)
avail_y=floor((20+qr_size-loc[1])/font_metrics[2][1])*font_metrics[2][1]
loc, excess=textbox(part["description"], loc, (label_width-loc[0]-10, avail_y), 2)
if excess!="":
loc=(10, loc[1])
loc, excess=textbox(excess, loc, (label_width-20, label_height-loc[1]), 2)
z.output("P1\n")

View File

View File

@@ -0,0 +1,177 @@
#!/usr/bin/env python
import requests
from zebra import Zebra
import qrcode
from math import ceil, floor
from PIL import Image
import textwrap
import argparse
# make substitutions for characters not in CP437
def subst(s):
repl={}
repl["®"]="(R)"
repl["©"]="(C)"
repl[""]="Ω" # U+2126 -> U+03A9, which is in CP437
repl["±"]="+/-"
out=""
for i in s:
try:
out=out+repl[i]
except:
out=out+i
return out
# filter out characters not in selected codepage
# (printer uses CP437)
def filter(s, cp):
out=""
for i in s:
try:
i.encode(cp)
out=out+i
except:
pass
return out
# handle escape characters in strings to be printed
def esc(s):
out=""
for i in s:
if i=="\"":
out=out+"\\\""
elif i=="\\":
out=out+"\\\\"
else:
out=out+i
return out
# render a line of text at coordinates
# return coordinates of next line
def textline(s, loc, fontnum):
z.output(f"A{loc[0]},{loc[1]},0,{fontnum},1,1,N,\"{esc(filter(subst(s), "cp437"))}\"\n")
return (loc[0], loc[1]+font_metrics[fontnum][1])
# wrap text in a bounding box at coordinates
# return coordinates of next line and any unused text
def textbox(s, loc, bbox, fontnum):
wrapped=textwrap.wrap(filter(subst(s), "cp437"), width=floor(bbox[0]/font_metrics[fontnum][0]))
line=0
while line*font_metrics[fontnum][1]<bbox[1] and line<len(wrapped):
loc=textline(wrapped[line], loc, fontnum)
line=line+1
return loc, " ".join(wrapped[line:])
# render a QR code at coordinates
# return size (single value, since QR codes are square)
def qr(s, loc, mul, brdr):
qr = qrcode.QRCode(
box_size=mul,
border=brdr,
)
qr.add_data(s)
qr.make(fit=True)
img = qr.make_image().copy()
padded=Image.new(mode="1", size=(8*ceil(img.width/8),img.height), color="white")
padded.paste(im=img, box=(0,0,img.width,img.height))
z.output(f"GW10,10,{ceil(padded.width/8)},{padded.height},{padded.tobytes().decode("cp437")}\n")
return img.height
def cli():
parser=argparse.ArgumentParser()
parser.add_argument("id", help="part ID", type=int)
parser.add_argument("-x", help="label width, in inches (default: 2\")", type=float)
parser.add_argument("-y", help="label height, in inches (default: 1\")", type=float)
parser.add_argument("-g", help="label gap, in inches (default: 0.118\")", type=float)
parser.add_argument("-q", help="send to selected print queue instead of stdout")
parser.add_argument("-p", help="PartDB base URL")
parser.add_argument("-k", help="PartDB API key")
parser.add_argument("-r", help="printer resolution (default: 203 dpi)", type=int)
parser.add_argument('-v', action='version', version='%(prog)s 0.1.2')
args=parser.parse_args()
id=args.id
if args.x==None:
label_width=2
else:
label_width=args.x
if args.y==None:
label_height=1
else:
label_height=args.y
if args.g==None:
label_gap=0.118
else:
label_gap=args.g
if args.q==None:
queue="zebra_python_unittest"
else:
queue=args.q
base_url=args.p
api_key=args.k
if args.r==None:
res=203
else:
res=args.r
if res!=203 and res!=300:
raise ValueError("valid resolution options are 203 and 300")
# width and height for built-in monospace fonts (includes whitespace)
font_metrics={}
if res==203:
font_metrics[1]=(10,14)
font_metrics[2]=(12,18)
font_metrics[3]=(14,22)
font_metrics[4]=(16,26)
font_metrics[5]=(34,50)
if res==300:
font_metrics[1]=(14,22)
font_metrics[2]=(18,30)
font_metrics[3]=(22,38)
font_metrics[4]=(26,46)
font_metrics[5]=(50,82)
# look up the part
url=f"{base_url}/api/parts/{id}"
headers={}
headers["Accept"]="application/json"
if api_key!=None:
headers["Authorization"]=f"Bearer {api_key}"
part=requests.get(url, headers=headers).json()
# render a label for it
label_width=floor(label_width*res)
label_height=floor(label_height*res)
label_gap=floor(label_gap*res)
z=Zebra(queue)
z.output(f"q{label_width}\n")
if (args.y!=None and args.g!=None):
z.output(f"Q{label_height},{label_gap}\n")
z.output("N\n")
if res==300:
qr_size=qr(f"{base_url}/en/part/{id}", (10, 10), 6, 3)
else:
qr_size=qr(f"{base_url}/en/part/{id}", (10, 10), 4, 2)
loc=(15+qr_size, 20)
loc=textline(part["ipn"], loc, 5)
loc, excess=textbox(f"{part["manufacturer"]["name"]} {part["manufacturer_product_number"]}", loc, (label_width-loc[0]-10, label_height-loc[1]-10), 2)
avail_y=floor((20+qr_size-loc[1])/font_metrics[2][1])*font_metrics[2][1]
loc, excess=textbox(part["description"], loc, (label_width-loc[0]-10, avail_y), 2)
if excess!="":
loc=(10, loc[1])
loc, excess=textbox(excess, loc, (label_width-20, label_height-loc[1]), 2)
z.output("P1\n")

View File

@@ -1,20 +1,22 @@
[build-system]
requires = ["flit_core >=3.2,<4"]
build-backend = "flit_core.buildapi"
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "partdb-labeler"
name = "partdb_labeler"
version = "0.2.2"
authors = [
{name="Scott Alfter", email="scott@alfter.us"}
{name = "Scott Alfter", email = "scott@alfter.us"},
]
description = "PartDB Labeler"
version = "0.1.0"
readme = "README.md"
dependencies = ["requests", "zebra", "qrcode", "pillow"]
[project.urls]
Home="https://gitlab.alfter.us/salfter/partdb-labeler"
[tool.flit.module]
name="partdb-labeler"
requires-python = ">=3.8"
dependencies = [
"requests",
"zebra",
"qrcode",
"pillow"
]
[project.scripts]
partdb_labeler = "partdb_labeler.partdb_labeler:cli"

2
setup.py Normal file
View File

@@ -0,0 +1,2 @@
from setuptools import setup
setup()