diff --git a/app/app.py b/app/app.py index 7849436..f8bcd3b 100644 --- a/app/app.py +++ b/app/app.py @@ -43,6 +43,7 @@ PRINT_QUEUE_ENABLED = os.environ.get("PRINT_QUEUE_ENABLED", "1").lower() in {"1" PRINT_QUEUE_RETRY_SECONDS = int(os.environ.get("PRINT_QUEUE_RETRY_SECONDS", "30")) DPI = int(os.environ.get("LABEL_DPI", "180")) PTOUCH_SUPPORTS_COPIES: bool | None = None +TEXT_FIT_EXTRA_MM = 6 TAPE_WIDTHS_MM = [3.5, 6, 9, 12, 18, 24] PRINTABLE_TAPE_PX = { @@ -111,6 +112,43 @@ def text_lines(text: str) -> list[str]: return text.split("\n") +def text_natural_width_mm(obj: dict[str, Any], scale: float) -> float: + font_size = scaled_mm_to_px(float(obj.get("fontSizeMm", 5)), scale) + font = get_font(font_size, bool(obj.get("bold", False))) + scratch = Image.new("1", (1, 1), 0) + scratch_draw = ImageDraw.Draw(scratch) + widths = [] + for line in text_lines(str(obj.get("text", "")) or " "): + box = scratch_draw.textbbox((0, 0), line or " ", font=font) + widths.append((box[2] - box[0]) / scale) + return max(2, max(widths, default=0) + TEXT_FIT_EXTRA_MM) + + +def autofit_text_objects_for_render(design: dict[str, Any], scale: float) -> dict[str, Any]: + if not bool(design.get("autoLength", True)): + return design + fitted = dict(design) + objects = [] + margin = float(fitted.get("marginMm", 2)) + for obj in fitted.get("objects", []): + if not isinstance(obj, dict): + objects.append(obj) + continue + item = dict(obj) + if item.get("type") in {"text", "symbol"} and not item.get("manualSize"): + item["w"] = text_natural_width_mm(item, scale) + objects.append(item) + fitted["objects"] = objects + length_end = margin + orientation = fitted.get("orientation", "landscape") + for obj in objects: + if not isinstance(obj, dict) or obj.get("hidden"): + continue + length_end = max(length_end, float(obj.get("y", 0)) + float(obj.get("h", 0)) if orientation == "portrait" else float(obj.get("x", 0)) + float(obj.get("w", 0))) + fitted["lengthMm"] = max(float(fitted.get("lengthMm", 70)), min(500, length_end + margin)) + return fitted + + def draw_text(image: Image.Image, draw: ImageDraw.ImageDraw, obj: dict[str, Any], scale: float) -> None: x, y, w, h = object_box(obj, scale) text = str(obj.get("text", "Text")) @@ -261,6 +299,8 @@ def render_design_image(design: dict[str, Any]) -> Image.Image: tape_width = float(design.get("tapeWidthMm", 24)) if tape_width not in TAPE_WIDTHS_MM: raise ValueError("Ungueltige Bandbreite.") + scale = render_scale_for_tape(tape_width) + design = autofit_text_objects_for_render(design, scale) length = clamp(float(design.get("lengthMm", 70)), 10, 500) orientation = design.get("orientation", "landscape") if orientation == "portrait": @@ -268,7 +308,6 @@ def render_design_image(design: dict[str, Any]) -> Image.Image: else: canvas_w_mm, canvas_h_mm = length, tape_width - scale = render_scale_for_tape(tape_width) image = new_label_image(scaled_mm_to_px(canvas_w_mm, scale), scaled_mm_to_px(canvas_h_mm, scale)) max_height_px = printable_px_for_tape(tape_width) if image.height > max_height_px: diff --git a/app/static/editor.js b/app/static/editor.js index df1966e..8d09215 100644 --- a/app/static/editor.js +++ b/app/static/editor.js @@ -8,6 +8,7 @@ const PRINTABLE_TAPE_PX = { 24: 128, }; const STORAGE_KEY = "ptouch-web-layout"; +const TEXT_FIT_EXTRA_MM = 6; const state = { zoom: 1.5, @@ -113,7 +114,7 @@ function textNaturalWidthMm(obj) { const lines = String(obj.text || " ").split("\n"); const maxTextWidthPx = Math.max(...lines.map((line) => ctx.measureText(line || " ").width), 0); ctx.restore(); - return Math.max(2, pxToMm(maxTextWidthPx) + 1); + return Math.max(2, pxToMm(maxTextWidthPx) + TEXT_FIT_EXTRA_MM); } function autoFitTextObject(obj) {