reduce label width

This commit is contained in:
Piotr Oleszczyk 2025-09-09 13:27:06 +02:00
parent 811e32e3da
commit 0e8c6ece0e

126
app.py
View file

@ -41,7 +41,7 @@ def mm_for_px(px, dpi=DPI):
@lru_cache(maxsize=256)
def make_code128_exact(
grocycode: str,
xdim_px: int = 4, # 46 px is a good, scannable range
xdim_px: int = 3, # narrower bars reduce overall width; ~0.25mm at 300dpi
dpi: int = DPI,
) -> Image.Image:
code = Code128(grocycode, writer=ImageWriter())
@ -150,76 +150,146 @@ def _ellipsize(
def make_text_panel(
product: str, due_date: Optional[str], max_width: int = 360, padding: int = 4
) -> Image.Image:
"""Render up to two lines of text (product, due_date) into a fixed-height strip.
"""Render product (wrapped to 2 lines) + due_date in a fixed-height strip.
- Keeps height = TARGET_H.
- Ellipsizes to ensure the panel width <= max_width.
- Chooses font sizes that fit vertically.
- Wraps product to up to 2 lines; ellipsizes overflow.
- Ellipsizes due_date; keeps panel width <= max_width.
"""
# Create a temporary image for measurement
meas_img = Image.new("L", (max_width, TARGET_H), 255)
draw = ImageDraw.Draw(meas_img)
# Try font size pairs, largest first, until both lines fit vertically
product_sizes = [22, 20, 18, 16, 14, 12, 11, 10, 9, 8]
# Try font size pairs, largest first, until content fits vertically
product_sizes = [28, 26, 24, 22, 20, 18, 16, 14, 12, 11, 10, 9, 8]
due_sizes = [14, 13, 12, 11, 10, 9, 8]
chosen = None
have_due = bool(due_date)
line_gap = 3
def text_h(f: ImageFont.ImageFont) -> int:
return f.getbbox("Ag")[3] if hasattr(f, "getbbox") else f.getsize("Ag")[1]
def wrap_ellipsize(
text: str, font: ImageFont.ImageFont, max_w: int, max_lines: int
) -> list[str]:
if not text:
return []
words = text.split()
lines: list[str] = []
i = 0
while len(lines) < max_lines:
line = ""
while i < len(words):
cand = (line + " " + words[i]).strip()
if draw.textlength(cand, font=font) <= max_w:
line = cand
i += 1
else:
if not line:
# Hard-break a very long word
low, high = 0, len(words[i])
best = ""
while low <= high:
mid = (low + high) // 2
seg = words[i][:mid]
if draw.textlength(seg, font=font) <= max_w:
best = seg
low = mid + 1
else:
high = mid - 1
line = best or words[i]
if best:
words[i] = words[i][len(best) :]
else:
i += 1
break
# If last allowed line and text remains, ellipsize tail into this line
if len(lines) == max_lines - 1 and i < len(words):
tail = (line + " " + " ".join(words[i:])).strip()
line = _ellipsize(tail, font, max_w, draw)
i = len(words)
if line:
lines.append(line)
else:
break
if i >= len(words):
break
return lines
for ps in product_sizes:
pf = _try_load_font(ps)
ph = pf.getbbox("Ag")[3] if hasattr(pf, "getbbox") else pf.getsize("Ag")[1]
ph = text_h(pf)
prod_lines = wrap_ellipsize(
product or "", pf, max_width - 2 * padding, max_lines=2
)
lines_count = max(1, len(prod_lines))
prod_block_h = ph * lines_count + line_gap * (lines_count - 1)
if have_due:
for ds in due_sizes:
df = _try_load_font(ds)
dh = (
df.getbbox("Ag")[3]
if hasattr(df, "getbbox")
else df.getsize("Ag")[1]
)
total_h = ph + dh + padding # small gap between lines
dh = text_h(df)
total_h = prod_block_h + line_gap + dh
if total_h + 2 * padding <= TARGET_H:
chosen = (pf, df)
chosen = (pf, df, prod_lines)
break
if chosen:
break
else:
if ph + 2 * padding <= TARGET_H:
chosen = (pf, None)
if prod_block_h + 2 * padding <= TARGET_H:
chosen = (pf, None, prod_lines)
break
if not chosen:
# Fallback to default tiny font(s)
chosen = (_try_load_font(8), _try_load_font(8) if have_due else None)
pf = _try_load_font(8)
df = _try_load_font(8) if have_due else None
prod_lines = wrap_ellipsize(
product or "", pf, max_width - 2 * padding, max_lines=2
)
chosen = (pf, df, prod_lines)
pf, df = chosen
pf, df, prod_lines = chosen
# Ellipsize to max_width
product_text = _ellipsize(product or "", pf, max_width - 2 * padding, draw)
# Prepare due text (single line, ellipsized)
due_text = (
_ellipsize(due_date or "", df or pf, max_width - 2 * padding, draw)
if have_due
else None
)
# Measure final sizes
p_w = int(draw.textlength(product_text, font=pf))
p_h = pf.getbbox("Ag")[3] if hasattr(pf, "getbbox") else pf.getsize("Ag")[1]
d_w = int(draw.textlength(due_text, font=df or pf)) if have_due else 0
d_h = (
# Measure and compute panel width
ph = pf.getbbox("Ag")[3] if hasattr(pf, "getbbox") else pf.getsize("Ag")[1]
dh = (
(df.getbbox("Ag")[3] if hasattr(df, "getbbox") else df.getsize("Ag")[1])
if have_due
else 0
)
p_w = max(
(int(draw.textlength(line, font=pf)) for line in (prod_lines or [""])),
default=0,
)
d_w = int(draw.textlength(due_text, font=df or pf)) if have_due else 0
panel_w = min(max(p_w, d_w) + 2 * padding, max_width)
# Render the panel
out = Image.new("L", (panel_w, TARGET_H), 255)
odraw = ImageDraw.Draw(out)
y = (TARGET_H - (p_h + (d_h + padding if have_due else 0))) // 2
odraw.text((padding, y), product_text, fill=0, font=pf)
content_h = ph * max(1, len(prod_lines)) + line_gap * (max(1, len(prod_lines)) - 1)
if have_due and due_text:
odraw.text((padding, y + p_h + padding), due_text, fill=0, font=df or pf)
content_h += line_gap + dh
y = (TARGET_H - content_h) // 2
# Draw product lines
for idx, line in enumerate(prod_lines or []):
odraw.text((padding, y + idx * (ph + line_gap)), line, fill=0, font=pf)
# Draw due date under product
if have_due and due_text:
prod_block_h = ph * max(1, len(prod_lines)) + line_gap * (
max(1, len(prod_lines)) - 1
)
odraw.text(
(padding, y + prod_block_h + line_gap), due_text, fill=0, font=df or pf
)
return out