Compare commits
	
		
			11 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| b14c855cc9 | |||
| f1a17cb4e7 | |||
| 3b7905b46d | |||
| c6d2974fea | |||
| 6c80b80aca | |||
| 990d37df99 | |||
| e80e88cb1c | |||
| 534d7ad615 | |||
| ce03d7b5b0 | |||
| 049c87869a | |||
| f02ca04c4c | 
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1 +1,4 @@ | ||||
| *~ | ||||
| partdb-labeler.bat | ||||
| partdb_labeler.egg-info | ||||
| build | ||||
|   | ||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							| @@ -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 %* | ||||
| ``` | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								dist/partdb_labeler-0.1.0-py2.py3-none-any.whl
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/partdb_labeler-0.1.0-py2.py3-none-any.whl
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								dist/partdb_labeler-0.1.0.tar.gz
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/partdb_labeler-0.1.0.tar.gz
									
									
									
									
										vendored
									
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -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") | ||||
							
								
								
									
										0
									
								
								partdb_labeler/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								partdb_labeler/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										177
									
								
								partdb_labeler/partdb_labeler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										177
									
								
								partdb_labeler/partdb_labeler.py
									
									
									
									
									
										Normal 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") | ||||
| @@ -1,20 +0,0 @@ | ||||
| [build-system] | ||||
| requires = ["flit_core >=3.2,<4"] | ||||
| build-backend = "flit_core.buildapi" | ||||
|  | ||||
| [project] | ||||
| name = "partdb-labeler" | ||||
| authors = [ | ||||
| 	{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" | ||||
|  | ||||
							
								
								
									
										24
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								setup.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| from setuptools import setup | ||||
| import sys | ||||
|  | ||||
| with open("README.md", "r") as fh: | ||||
|     long_description = fh.read() | ||||
|  | ||||
| setup( | ||||
|     name="partdb_labeler", | ||||
|     version="0.2.1", | ||||
|     description="PartDB Labeler", | ||||
|     long_description=long_description, | ||||
|     long_description_content_type="text/markdown", | ||||
|     author="Scott Alfter", | ||||
|     author_email="scott@alfter.us", | ||||
|     url="https://gitlab.alfter.us/salfter/partdb-labeler", | ||||
|     py_modules=["partdb_labeler.partdb_labeler"], | ||||
|     install_requires=[ | ||||
|         "requests",  | ||||
|         "zebra",  | ||||
|         "qrcode",  | ||||
|         "pillow" | ||||
|     ], | ||||
|     entry_points={"console_scripts": ["partdb_labeler = partdb_labeler.partdb_labeler:cli"]} | ||||
| ) | ||||
		Reference in New Issue
	
	Block a user