this packaging seems to work
This commit is contained in:
		
							
								
								
									
										27
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,5 +1,28 @@ | ||||
| partdb-labeler | ||||
| ============== | ||||
|  | ||||
| Connects to partdb.alfter.us, grabs a selected part, and formats a label to | ||||
| be printed on an EPL2-compatible printer (such as the Zebra LP2844). | ||||
|  | ||||
|  | ||||
| Connects to a [PartDB](https://github.com/Part-DB/Part-DB-server) server, grabs info for a selected part, and formats a label to | ||||
| be printed on an EPL2-compatible label printer.  Command-line options enable configuration of the label size, server to use,  | ||||
| and more. | ||||
|  | ||||
| Why did I write this? | ||||
| --------------------- | ||||
|  | ||||
| PartDB has a label generator built-in, but it only produces PDFs that must then be rasterized and printed.  This works well  | ||||
| enough if you're sending labels to a laser printer, but I found that QR codes small enough to fit on the label stock I wanted | ||||
| to use weren't scannable.  This generator speaks EPL2, one of the languages used by Zebra label printers, to produce  | ||||
| precisely-formatted labels with QR codes that scan easily with your phone or a dedicated barcode scanner. | ||||
|  | ||||
| Compatibility | ||||
| ------------- | ||||
|  | ||||
| So far, it's been tested with two Zebra printers: an LP2844 and a GK420t.  The LP2844 was driven by an Arch Linux system with | ||||
| a CUPS print queue feeding it through a network print server.  The GK420t was driven by a Windows 11 system, connected to the | ||||
| printer via USB.   | ||||
|  | ||||
| Usage | ||||
| ----- | ||||
|  | ||||
| ```python -m partdb-labeler -h``` will show you the available options. | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								dist/partdb_labeler-0.1.0-py2.py3-none-any.whl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/partdb_labeler-0.1.0-py2.py3-none-any.whl
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								dist/partdb_labeler-0.1.0.tar.gz
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								dist/partdb_labeler-0.1.0.tar.gz
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								doc/example.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								doc/example.jpg
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 105 KiB | 
							
								
								
									
										87
									
								
								label.py
									
									
									
									
									
								
							
							
						
						
									
										87
									
								
								label.py
									
									
									
									
									
								
							| @@ -1,87 +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 | ||||
| from sys import argv | ||||
|  | ||||
| # make substitutions for characters not in CP437 | ||||
|  | ||||
| def subst(s): | ||||
|     repl={} | ||||
|     repl["®"]="(R)" | ||||
|     repl["©"]="(C)" | ||||
|     repl["Ω"]="Ω" # U+2126 -> U+03A9, which is in CP437 | ||||
|     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 | ||||
|  | ||||
| # render a line of text at coordinates | ||||
| # return coordinates of next line | ||||
|  | ||||
| def textline(s, loc, fontnum): | ||||
|     pass | ||||
|  | ||||
| label_width=2 # inches | ||||
| label_height=1 #inches | ||||
| id=argv[1] | ||||
| api_key="tcp_673fc81f0b7837ca4c029fbd6536b27742eb8b742eba27bf547c8136dc6a84f8" # anonymous read-only | ||||
|  | ||||
| url=f"https://partdb.alfter.us/api/parts/{id}" | ||||
| headers={} | ||||
| headers["Accept"]="application/json" | ||||
| headers["Authorization"]=f"Bearer {api_key}" | ||||
| part=requests.get(url, headers=headers).json() | ||||
|  | ||||
| # print(f"name: {part["name"]}") | ||||
| # print(f"description: {part["description"]}") | ||||
| # print(f"manufacturer: {part["manufacturer"]["name"]}") | ||||
| # print(f"MPN: {part["manufacturer_product_number"]}") | ||||
| # print(f"IPN: {part["ipn"]}") | ||||
| # print(f"URL: https://partdb.alfter.us/en/part/{id}") | ||||
|  | ||||
| z=Zebra("lp2844") | ||||
| z.output(f"q{floor(203*label_width)}\n") #, label_height=(floor(203*label_height),0)) | ||||
| z.output("N\n") | ||||
|  | ||||
| qr = qrcode.QRCode( | ||||
|     box_size=4, | ||||
|     border=2, | ||||
| ) | ||||
| qr.add_data(url) | ||||
| 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") | ||||
|  | ||||
| x=15+8*ceil(padded.width/8) | ||||
| z.output(f"A{x},20,0,5,1,1,N,\"{part["ipn"]}\"\n") | ||||
| z.output(f"A{x},80,0,2,1,1,N,\"{filter(part["manufacturer_product_number"], "cp437")}\"\n") | ||||
| z.output(f"A{x},98,0,2,1,1,N,\"{filter(part["manufacturer"]["name"], "cp437")}\"\n") | ||||
|  | ||||
| y=116+28 | ||||
| for line in textwrap.wrap(filter(subst(part["description"]), "cp437"), width=floor((floor(203*label_width)-40)/10)): | ||||
|     z.output(f"A{10},{y},0,1,1,1,N,\"{line}\"\n") | ||||
|     y=y+14 | ||||
|  | ||||
| z.output("P1\n") | ||||
							
								
								
									
										174
									
								
								partdb-labeler.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								partdb-labeler.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,174 @@ | ||||
| #!/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") | ||||
							
								
								
									
										20
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								pyproject.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| [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" | ||||
|  | ||||
		Reference in New Issue
	
	Block a user