Files
cc-1c-skills/.claude/skills/img-grid/scripts/overlay-grid.py
T
78b5b73fa7 fix(skills): db-load-git кириллические пути (#32) + img-grid валидация (#33)
#32: git diff/ls-files при дефолтном core.quotePath=true возвращают кириллицу
в octal-виде → объект с кириллическим именем не распознавался в partial-load.
Все git-вызовы теперь с `-c core.quotePath=false`. PY — в центральном run_git;
PS — унифицировал inline-вызовы в хелпер Invoke-GitLines (паритет с py). v1.11.

#33: img-grid падал ZeroDivisionError при -c 0 / некорректном -r. Добавлена
валидация (cols>0, rows>=0, auto-rows max(1,...)). Pillow задокументирован как
runtime-зависимость (README + python-porting-guide). overlay-grid.py получил
версионный заголовок (v1.1).

Тесты: runner error-кейсы db-dump-dt/db-load-dt/db-load-git (из PR #34).
img-grid runner-кейс не вводили (py-only + Pillow-зависимость) — #33 проверен вручную.

Closes #32, #33

Co-Authored-By: Korolev Pavel <korolev.vrn@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:33:10 +03:00

124 lines
4.1 KiB
Python

#!/usr/bin/env python3
# img-grid v1.1 — Overlay numbered grid on image
# Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
"""Overlay a numbered grid on an image to help determine column/row proportions.
Usage: python overlay-grid.py <image> [-c COLS] [-r ROWS] [-o OUTPUT]
The grid helps an LLM count "squares" to determine exact column widths
and positions when analyzing printed forms for MXL template generation.
Numbers are rendered in a dedicated margin band outside the image content,
so they never overlap with the form and remain readable at any grid density.
"""
import argparse
import os
import sys
from PIL import Image, ImageDraw, ImageFont
MARGIN_TOP = 20
MARGIN_LEFT = 24
def main():
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
parser = argparse.ArgumentParser(description="Overlay numbered grid on image")
parser.add_argument("image", help="Input image path")
parser.add_argument("-c", "--cols", type=int, default=50,
help="Number of vertical divisions (default: 50)")
parser.add_argument("-r", "--rows", type=int, default=0,
help="Number of horizontal divisions (0 = auto, match cell aspect ratio)")
parser.add_argument("-o", "--output", help="Output path (default: <name>-grid.<ext>)")
args = parser.parse_args()
if args.cols <= 0:
parser.error("--cols must be greater than 0")
if args.rows < 0:
parser.error("--rows must be greater than or equal to 0")
src = Image.open(args.image).convert("RGBA")
sw, sh = src.size
cols = args.cols
step_x = sw / cols
rows = args.rows
if rows == 0:
rows = max(1, round(sh / step_x))
step_y = sh / rows
# Canvas with margins for labels
cw = MARGIN_LEFT + sw
ch = MARGIN_TOP + sh
canvas = Image.new("RGBA", (cw, ch), (255, 255, 255, 255))
canvas.paste(src, (MARGIN_LEFT, MARGIN_TOP))
overlay = Image.new("RGBA", (cw, ch), (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay)
# Font for labels in margin
label_font_size = 12
try:
label_font = ImageFont.truetype("arial.ttf", label_font_size)
except Exception:
label_font = ImageFont.load_default()
# --- Vertical lines + numbers in top margin ---
for i in range(cols + 1):
x = MARGIN_LEFT + round(i * step_x)
major = i % 10 == 0
mid = i % 5 == 0
alpha = 160 if major else (110 if mid else 40)
lw = 2 if major else 1
draw.line([(x, MARGIN_TOP), (x, ch)], fill=(255, 0, 0, alpha), width=lw)
# Labels: always show multiples of 5; show all if spacing allows
show_label = major or mid or step_x >= 20
if show_label:
label = str(i)
bbox = label_font.getbbox(label)
tw = bbox[2] - bbox[0]
tx = x - tw // 2
ty = 2
color = (200, 0, 0, 255) if (major or mid) else (200, 0, 0, 180)
draw.text((tx, ty), label, fill=color, font=label_font)
# --- Horizontal lines + numbers in left margin ---
for j in range(rows + 1):
y = MARGIN_TOP + round(j * step_y)
major = j % 10 == 0
mid = j % 5 == 0
alpha = 160 if major else (110 if mid else 20)
lw = 2 if major else 1
draw.line([(MARGIN_LEFT, y), (cw, y)], fill=(0, 0, 200, alpha), width=lw)
show_label = major or mid or step_y >= 20
if show_label:
label = str(j)
bbox = label_font.getbbox(label)
tw = bbox[2] - bbox[0]
tx = MARGIN_LEFT - tw - 3
ty = y - label_font_size // 2
color = (0, 0, 200, 255) if (major or mid) else (0, 0, 200, 180)
draw.text((tx, ty), label, fill=color, font=label_font)
result = Image.alpha_composite(canvas, overlay).convert("RGB")
if args.output:
out = args.output
else:
name, ext = os.path.splitext(args.image)
out = f"{name}-grid{ext}"
result.save(out)
print(f"Grid: {cols} x {rows} cells")
print(f"Cell size: {step_x:.1f} x {step_y:.1f} px")
print(f"Image: {sw} x {sh} px")
print(f"Saved: {out}")
if __name__ == "__main__":
main()