reduce label width
This commit is contained in:
parent
811e32e3da
commit
0e8c6ece0e
1 changed files with 98 additions and 28 deletions
126
app.py
126
app.py
|
|
@ -41,7 +41,7 @@ def mm_for_px(px, dpi=DPI):
|
||||||
@lru_cache(maxsize=256)
|
@lru_cache(maxsize=256)
|
||||||
def make_code128_exact(
|
def make_code128_exact(
|
||||||
grocycode: str,
|
grocycode: str,
|
||||||
xdim_px: int = 4, # 4–6 px is a good, scannable range
|
xdim_px: int = 3, # narrower bars reduce overall width; ~0.25mm at 300dpi
|
||||||
dpi: int = DPI,
|
dpi: int = DPI,
|
||||||
) -> Image.Image:
|
) -> Image.Image:
|
||||||
code = Code128(grocycode, writer=ImageWriter())
|
code = Code128(grocycode, writer=ImageWriter())
|
||||||
|
|
@ -150,76 +150,146 @@ def _ellipsize(
|
||||||
def make_text_panel(
|
def make_text_panel(
|
||||||
product: str, due_date: Optional[str], max_width: int = 360, padding: int = 4
|
product: str, due_date: Optional[str], max_width: int = 360, padding: int = 4
|
||||||
) -> Image.Image:
|
) -> 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.
|
- Keeps height = TARGET_H.
|
||||||
- Ellipsizes to ensure the panel width <= max_width.
|
- Wraps product to up to 2 lines; ellipsizes overflow.
|
||||||
- Chooses font sizes that fit vertically.
|
- Ellipsizes due_date; keeps panel width <= max_width.
|
||||||
"""
|
"""
|
||||||
# Create a temporary image for measurement
|
# Create a temporary image for measurement
|
||||||
meas_img = Image.new("L", (max_width, TARGET_H), 255)
|
meas_img = Image.new("L", (max_width, TARGET_H), 255)
|
||||||
draw = ImageDraw.Draw(meas_img)
|
draw = ImageDraw.Draw(meas_img)
|
||||||
|
|
||||||
# Try font size pairs, largest first, until both lines fit vertically
|
# Try font size pairs, largest first, until content fits vertically
|
||||||
product_sizes = [22, 20, 18, 16, 14, 12, 11, 10, 9, 8]
|
product_sizes = [28, 26, 24, 22, 20, 18, 16, 14, 12, 11, 10, 9, 8]
|
||||||
due_sizes = [14, 13, 12, 11, 10, 9, 8]
|
due_sizes = [14, 13, 12, 11, 10, 9, 8]
|
||||||
chosen = None
|
chosen = None
|
||||||
have_due = bool(due_date)
|
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:
|
for ps in product_sizes:
|
||||||
pf = _try_load_font(ps)
|
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:
|
if have_due:
|
||||||
for ds in due_sizes:
|
for ds in due_sizes:
|
||||||
df = _try_load_font(ds)
|
df = _try_load_font(ds)
|
||||||
dh = (
|
dh = text_h(df)
|
||||||
df.getbbox("Ag")[3]
|
total_h = prod_block_h + line_gap + dh
|
||||||
if hasattr(df, "getbbox")
|
|
||||||
else df.getsize("Ag")[1]
|
|
||||||
)
|
|
||||||
total_h = ph + dh + padding # small gap between lines
|
|
||||||
if total_h + 2 * padding <= TARGET_H:
|
if total_h + 2 * padding <= TARGET_H:
|
||||||
chosen = (pf, df)
|
chosen = (pf, df, prod_lines)
|
||||||
break
|
break
|
||||||
if chosen:
|
if chosen:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if ph + 2 * padding <= TARGET_H:
|
if prod_block_h + 2 * padding <= TARGET_H:
|
||||||
chosen = (pf, None)
|
chosen = (pf, None, prod_lines)
|
||||||
break
|
break
|
||||||
|
|
||||||
if not chosen:
|
if not chosen:
|
||||||
# Fallback to default tiny font(s)
|
# 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
|
# Prepare due text (single line, ellipsized)
|
||||||
product_text = _ellipsize(product or "", pf, max_width - 2 * padding, draw)
|
|
||||||
due_text = (
|
due_text = (
|
||||||
_ellipsize(due_date or "", df or pf, max_width - 2 * padding, draw)
|
_ellipsize(due_date or "", df or pf, max_width - 2 * padding, draw)
|
||||||
if have_due
|
if have_due
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
|
|
||||||
# Measure final sizes
|
# Measure and compute panel width
|
||||||
p_w = int(draw.textlength(product_text, font=pf))
|
ph = pf.getbbox("Ag")[3] if hasattr(pf, "getbbox") else pf.getsize("Ag")[1]
|
||||||
p_h = pf.getbbox("Ag")[3] if hasattr(pf, "getbbox") else pf.getsize("Ag")[1]
|
dh = (
|
||||||
d_w = int(draw.textlength(due_text, font=df or pf)) if have_due else 0
|
|
||||||
d_h = (
|
|
||||||
(df.getbbox("Ag")[3] if hasattr(df, "getbbox") else df.getsize("Ag")[1])
|
(df.getbbox("Ag")[3] if hasattr(df, "getbbox") else df.getsize("Ag")[1])
|
||||||
if have_due
|
if have_due
|
||||||
else 0
|
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)
|
panel_w = min(max(p_w, d_w) + 2 * padding, max_width)
|
||||||
|
|
||||||
# Render the panel
|
# Render the panel
|
||||||
out = Image.new("L", (panel_w, TARGET_H), 255)
|
out = Image.new("L", (panel_w, TARGET_H), 255)
|
||||||
odraw = ImageDraw.Draw(out)
|
odraw = ImageDraw.Draw(out)
|
||||||
y = (TARGET_H - (p_h + (d_h + padding if have_due else 0))) // 2
|
content_h = ph * max(1, len(prod_lines)) + line_gap * (max(1, len(prod_lines)) - 1)
|
||||||
odraw.text((padding, y), product_text, fill=0, font=pf)
|
|
||||||
if have_due and due_text:
|
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
|
return out
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue