Skip to content

Instantly share code, notes, and snippets.

@xela-zone
Last active September 19, 2025 22:36
Show Gist options
  • Save xela-zone/03fbfbc33885ccb3a1a46629fd82a672 to your computer and use it in GitHub Desktop.
Save xela-zone/03fbfbc33885ccb3a1a46629fd82a672 to your computer and use it in GitHub Desktop.
generates QR codes with letter and number pairs, saves to a PDF
from PIL import Image
from PIL import ImageDraw
from qrcode.main import QRCode
from qrcode.constants import ERROR_CORRECT_L
from PIL import ImageFont
from typing import List, Sequence, Generator, Tuple, TypeVar
import math
import shutil
import time
import multiprocessing as mp
t = time.process_time()
_T = TypeVar("_T")
def grouper(iterable: Sequence[_T], n: int) -> Generator[Sequence[_T], None, None]:
"""
given a iterable, yield that iterable back in chunks of size n. last item will be any size.
"""
for i in range(math.ceil(len(iterable) / n)):
yield iterable[i * n : i * n + n]
FONT = ImageFont.truetype("font.ttf", 500)
FONT_SMALLER = ImageFont.truetype("font.ttf", 300)
requests: List[Tuple[str, int]] = []
HEIGHT = 2 # inches
locators: List[Tuple[str, Tuple[int, int], int]] = [
("A", (1, 4), 1),
("B", (1, 12), 1),
("C", (1, 12), 1),
("D", (1, 12), 1),
("E", (1, 12), 1),
("F", (1, 12), 1),
("G", (1, 8), 1),
]
# #################################
# locators = [
# ("Single Orders", (1, 20), 1),
# ("GMD", (1, 6), 1),
# ("O", (1, 14), 1),
# ("Z", (1, 23), 2),
# ("Q", (1, 21), 1),
# ]
# HEIGHT = 1.75
# #################################
for entry in locators:
letter = entry[0]
count = entry[1]
if len(entry) != 3:
entry = (letter, count, 1)
for code in range(entry[2]):
for number in range(count[0], count[1] + 1):
requests.append((letter, number))
images = []
cover = Image.new("RGB", (2550, 3300), color=(255, 255, 255))
text = ""
for letter, locatorsRange, count in locators:
text += f"{letter}: {locatorsRange[0]}-{locatorsRange[1]} X {count}\n"
ImageDraw.Draw(cover).text(
(75, 75), f"{len(requests)} QR Codes\n{text}", font_size=100, fill="black"
)
images.append(cover)
cover.save("test.png")
# solve how many can i fit on a page?
# page dims are 8.5x11 inches, with a 0.25 inch margin
# 8.5 - 2*0.25 = 8 inches wide
# 11 - 2*0.25 = 10.5 inches tall
# we want each qr code to be HEIGHT inches tall, and 1.6 * HEIGHT inches wide. 1.6 is the aspect ratio of the qr code.
# so we can fit 10.5 / HEIGHT qr codes tall, and 8 / (1.6 * HEIGHT) qr codes wide
WIDTH = 1.6 * HEIGHT
per_vert = math.floor(10.5 / HEIGHT)
per_horiz = math.floor(8 / WIDTH)
# we should check if we can fit more qr codes on the page by rotating the QR codes
extra_sideways = 0
if per_horiz * WIDTH < 8 - HEIGHT:
# how many extra?
extra_sideways = math.ceil((10.5 - WIDTH) / WIDTH)
def generate_qr(data: Tuple[str, int]) -> Image.Image:
single = Image.new("RGB", (1600, 1000), color=(255, 255, 255))
letter = str(data[0])
number = data[1]
qr = QRCode(
version=1,
error_correction=ERROR_CORRECT_L,
box_size=50,
border=0,
)
qr.add_data(f"{letter}-{number}")
qr.make(fit=True)
qr_img = qr.make_image(fill_color="black", back_color="white")
single.paste(
qr_img.resize((800, 800)),
(110, 110),
)
ImageDraw.Draw(single).rectangle( # Border line
((0, 0, 1600, 1000)),
fill=None,
outline="black",
width=20,
)
if len(letter) > 3:
# oh god, it's a string lable! get the first Char, and the first Char After a space
first_char = letter[0]
second_char = ""
for char, i in zip(letter, range(len(letter))):
if char == " " and i + 1 < len(letter):
second_char = letter[i + 1]
break
# ok, we have two letters, let's overrive letter with them
letter = first_char + second_char
if len(letter) == 2 or len(letter) == 1:
ImageDraw.Draw(single).text(
(975, 0),
letter[-2] if len(letter) > 1 else "",
font=FONT,
fill="black",
)
ImageDraw.Draw(single).text(
(1275, 0),
letter[-1],
font=FONT,
fill="black",
) # letter[-1]
elif len(letter) == 3:
ImageDraw.Draw(single).text(
(975, 125),
letter,
font=FONT_SMALLER,
fill="black",
)
# First Number
ImageDraw.Draw(single).text(
(975, 425),
f"{(number // 10) % 10}",
font=FONT,
fill="black", # f"{(number // 10) % 10}"
)
# Second Number
ImageDraw.Draw(single).text(
(1275, 425),
f"{number % 10}",
font=FONT,
fill="black", # f"{number % 10}"
)
return single
def page_builder(group):
page = Image.new("RGB", (2550, 3300), color=(255, 255, 255))
grid, extra = group[: per_vert * per_horiz], group[per_vert * per_horiz :]
for i, graphic in enumerate(grid):
x = ((i % per_horiz) * int(HEIGHT * 300 * 1.6)) + 75
y = ((i // per_horiz) * int(HEIGHT * 300)) + 75
page.paste(
graphic.resize((int(HEIGHT * 300 * 1.6), int(HEIGHT * 300))),
(x, y),
)
for i, graphic in enumerate(extra):
# starting from the right edge of the grid , go down the page, sideways
x = per_horiz * int(HEIGHT * 300 * 1.6) + 75
y = (i * int(HEIGHT * 1.6 * 300)) + 75
g = graphic.resize((int(HEIGHT * 300 * 1.6), int(HEIGHT * 300)))
g = g.rotate(90, expand=True)
page.paste(
g,
(x, y),
)
return page
if __name__ == "__main__":
mp.freeze_support()
# we can fit a total of per_vert * per_horiz + extra_sideways qr codes on the page
print(
f"Height: {HEIGHT}, per_vert: {per_vert}, per_horiz: {per_horiz}, extra_sideways: {extra_sideways}, total: {per_vert * per_horiz + extra_sideways}/page, {math.ceil(len(requests)/(per_vert * (per_horiz + extra_sideways)))} total pages for {len(requests)} qr codes."
)
print(f"Time PRE CALC DONE: {time.process_time() - t}")
requested_graphics = mp.Pool().map(generate_qr, requests)
print(f"Time QR GENERATED: {time.process_time() - t}")
images = mp.Pool().map(
page_builder,
grouper(requested_graphics, per_vert * per_horiz + extra_sideways),
)
print(f"Time START PDF GEN: {time.process_time() - t}")
images[0].save("out_test_1.png")
images[0].save(
"out_all.pdf",
save_all=True,
append_images=images[1:],
resolution=100.0,
)
shutil.make_archive("qrcodes", "zip", ".", "out_all.pdf")
print(f"Time: {time.process_time() - t}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment