From 0e8c6ece0e4bb341ca53e29704ba6652bca9c9fa Mon Sep 17 00:00:00 2001 From: Piotr Oleszczyk Date: Tue, 9 Sep 2025 13:27:06 +0200 Subject: [PATCH] reduce label width --- app.py | 126 ++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 98 insertions(+), 28 deletions(-) diff --git a/app.py b/app.py index b6f49b6..2f79c12 100644 --- a/app.py +++ b/app.py @@ -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, # 4–6 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