From 044bc189741e496b859cd734622fabb3caff9167 Mon Sep 17 00:00:00 2001 From: Nick Shirokov Date: Sun, 8 Feb 2026 22:35:25 +0300 Subject: [PATCH] Add img-grid skill and page auto-sizing to mxl-compile - New skill /img-grid: overlays numbered grid on images to help determine column proportions for MXL template generation - Add "page" field to MXL DSL ("A4-landscape", "A4-portrait", or number) that auto-calculates defaultWidth from column proportions - Update DSL spec, mxl-compile SKILL.md, MXL guide, README Co-Authored-By: Claude Opus 4.6 --- .claude/skills/img-grid/SKILL.md | 77 ++++++++++++ .../skills/img-grid/scripts/overlay-grid.py | 112 ++++++++++++++++++ .claude/skills/mxl-compile/SKILL.md | 5 +- .../mxl-compile/scripts/mxl-compile.ps1 | 54 +++++++++ README.md | 4 +- docs/mxl-dsl-spec.md | 3 +- docs/mxl-guide.md | 16 +++ 7 files changed, 268 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/img-grid/SKILL.md create mode 100644 .claude/skills/img-grid/scripts/overlay-grid.py diff --git a/.claude/skills/img-grid/SKILL.md b/.claude/skills/img-grid/SKILL.md new file mode 100644 index 00000000..25d17eb3 --- /dev/null +++ b/.claude/skills/img-grid/SKILL.md @@ -0,0 +1,77 @@ +--- +name: img-grid +description: Наложить пронумерованную сетку на изображение для определения пропорций колонок +argument-hint: [-c COLS] +allowed-tools: + - Bash + - Read +--- + +# /img-grid — Сетка для анализа макета + +Накладывает пронумерованную сетку на изображение печатной формы. Позволяет точно определить границы колонок, их пропорции и span-ы для генерации макета табличного документа. + +## Использование + +``` +/img-grid [-c COLS] [-o OUTPUT] +``` + +## Параметры + +| Параметр | Обязательный | По умолчанию | Описание | +|-----------|:------------:|--------------|-----------------------------------------------| +| ImagePath | да | — | Путь к изображению (PNG, JPG) | +| -c COLS | нет | 50 | Количество вертикальных делений | +| -r ROWS | нет | авто | Количество горизонтальных делений (авто = квадратные ячейки) | +| -o OUTPUT | нет | `-grid.` | Путь для результата | + +## Команда + +```bash +python .claude/skills/img-grid/scripts/overlay-grid.py "" [-c 50] [-o ""] +``` + +Требуется Python 3 с библиотекой Pillow (`pip install Pillow`). + +## Что делает + +1. Рисует полупрозрачные вертикальные (красные) и горизонтальные (синие) линии +2. Нумерует линии в отдельных полях сверху и слева (не перекрывает содержимое) +3. Каждая 5-я и 10-я линия выделены ярче для удобства счёта + +## Как использовать результат + +### 1. Определить границы колонок + +Посмотреть на изображение с сеткой и записать координаты вертикальных границ каждой колонки таблицы (в номерах grid-линий). + +### 2. Найти базовую решётку + +Если на форме несколько таблиц с разной раскладкой (например, шапка документа и основная таблица), объединить все граничные точки. Каждый сегмент между соседними границами — одна колонка MXL. + +Пример для М-11: +- Шапка: границы 0, 2, 4, 9, 14, 21, 28, 34, 40, 48 +- Таблица: границы 0, 2, 4, 11, 16, 19, 23, 28, 32, 36, 42, 48 +- Объединение: 0, 2, 4, 9, 11, 14, 16, 19, 21, 23, 28, 32, 34, 36, 40, 42, 48 +- Результат: **16 базовых колонок** с пропорциями 2, 2, 5, 2, 3, 2, 3, 2, 2, 5, 4, 2, 2, 4, 2, 6 + +### 3. Записать в JSON DSL + +```json +{ + "columns": 16, + "page": "A4-landscape", + "columnWidths": { + "1": "2x", "2": "2x", "3": "5x", "4": "2x", "5": "3x", + "6": "2x", "7": "3x", "8": "2x", "9": "2x", "10": "5x", + "11": "4x", "12": "2x", "13": "2x", "14": "4x", "15": "2x", "16": "6x" + } +} +``` + +Поле `"page"` позволяет компилятору автоматически вычислить абсолютные ширины из пропорций. + +### 4. Скомпилировать + +`/mxl-compile` → `/mxl-validate` → `/mxl-info` diff --git a/.claude/skills/img-grid/scripts/overlay-grid.py b/.claude/skills/img-grid/scripts/overlay-grid.py new file mode 100644 index 00000000..b692b39b --- /dev/null +++ b/.claude/skills/img-grid/scripts/overlay-grid.py @@ -0,0 +1,112 @@ +"""Overlay a numbered grid on an image to help determine column/row proportions. + +Usage: python overlay-grid.py [-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 +from PIL import Image, ImageDraw, ImageFont + +MARGIN_TOP = 20 +MARGIN_LEFT = 24 + + +def main(): + 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: -grid.)") + args = parser.parse_args() + + 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 = 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() diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index af08a49b..6acb41f0 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -39,6 +39,8 @@ powershell.exe -NoProfile -File .claude/skills/mxl-compile/scripts/mxl-compile.p 3. Claude вызывает `/mxl-validate` для проверки корректности 4. Claude вызывает `/mxl-info` для верификации структуры +**Если макет создаётся по изображению** (скриншот, скан печатной формы) — сначала вызвать `/img-grid` для наложения сетки, по ней определить границы колонок и пропорции, затем использовать `"Nx"` ширины + `"page"` для автоматического расчёта размеров. + ## JSON-схема DSL Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool перед написанием JSON). @@ -46,7 +48,7 @@ powershell.exe -NoProfile -File .claude/skills/mxl-compile/scripts/mxl-compile.p Краткая структура: ``` -{ columns, defaultWidth, columnWidths, +{ columns, page, defaultWidth, columnWidths, fonts: { name: { face, size, bold, italic, underline, strikeout } }, styles: { name: { font, align, valign, border, borderWidth, wrap, format } }, areas: [{ name, rows: [{ height, rowStyle, cells: [ @@ -56,6 +58,7 @@ powershell.exe -NoProfile -File .claude/skills/mxl-compile/scripts/mxl-compile.p ``` Ключевые правила: +- `page` — формат страницы (`"A4-landscape"`, `"A4-portrait"` или число). Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` - `col` — 1-based позиция колонки - `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине) - Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index 9e9a208f..5159cd6c 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -119,6 +119,57 @@ function Parse-ColumnSpec { return $cols } +# --- 4a. Auto-calculate defaultWidth from page format --- + +$pageTargets = @{ + "A4-landscape" = 780 + "A4-portrait" = 540 +} + +if ($def.page) { + $pageName = "$($def.page)" + $targetWidth = $null + + if ($pageName -match '^\d+$') { + $targetWidth = [int]$pageName + } elseif ($pageTargets.ContainsKey($pageName)) { + $targetWidth = $pageTargets[$pageName] + } else { + Write-Warning "Unknown page format '$pageName'. Known: $($pageTargets.Keys -join ', '), or a number." + } + + if ($targetWidth) { + $totalUnits = 0.0 + $absoluteSum = 0 + $specifiedCols = @{} + + if ($def.columnWidths) { + foreach ($prop in $def.columnWidths.PSObject.Properties) { + $val = "$($prop.Value)" + $cols = Parse-ColumnSpec $prop.Name + foreach ($c in $cols) { + $specifiedCols[[int]$c] = $true + if ($val -match '^([0-9.]+)x$') { + $totalUnits += [double]$Matches[1] + } else { + $absoluteSum += [int]$val + } + } + } + } + + for ($c = 1; $c -le $totalColumns; $c++) { + if (-not $specifiedCols.ContainsKey($c)) { + $totalUnits += 1.0 + } + } + + if ($totalUnits -gt 0) { + $defaultWidth = [int][math]::Round(($targetWidth - $absoluteSum) / $totalUnits) + } + } +} + # Build column width map: 1-based col -> width $colWidthMap = @{} if ($def.columnWidths) { @@ -671,6 +722,9 @@ $enc = New-Object System.Text.UTF8Encoding($true) # --- 9. Summary --- Write-Host "[OK] Compiled: $OutputPath" +if ($def.page) { + Write-Host " Page: $pageName -> target $targetWidth, defaultWidth=$defaultWidth" +} Write-Host " Areas: $($namedItems.Count), Rows: $totalRowCount, Columns: $totalColumns" Write-Host " Fonts: $($fontEntries.Count), Lines: $lineCount, Formats: $($formatRegistry.Count)" Write-Host " Merges: $($merges.Count)" diff --git a/README.md b/README.md index 5d3dc842..f42a918f 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ |--------|--------|----------|------| | Внешние обработки (EPF) | 10 навыков `/epf-*` | Создание, модификация, сборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | +| Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — | ## Требования @@ -53,7 +54,8 @@ ├── mxl-info/ # Анализ макета ├── mxl-validate/ # Валидация макета ├── mxl-compile/ # Компиляция макета из JSON -└── mxl-decompile/ # Декомпиляция макета в JSON +├── mxl-decompile/ # Декомпиляция макета в JSON +└── img-grid/ # Сетка для анализа изображений docs/ ├── epf-guide.md # Гайд: внешние обработки ├── mxl-guide.md # Гайд: табличный документ diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index 98079a9d..53b84737 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -74,7 +74,8 @@ | Поле | Обяз. | По умолч. | Описание | |------|:-----:|-----------|----------| | `columns` | да | — | Количество колонок | -| `defaultWidth` | нет | 10 | Ширина колонок по умолчанию | +| `page` | нет | — | Формат страницы: `"A4-landscape"` (780), `"A4-portrait"` (540) или число. Автоматически вычисляет `defaultWidth` из суммы пропорций `"Nx"` | +| `defaultWidth` | нет | 10 | Ширина колонок по умолчанию. Игнорируется если задан `page` и все колонки используют `"Nx"` | | `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"`. Значения: число (абсолют) или `"Nx"` (множитель от defaultWidth, напр. `"2x"`, `"0.5x"`) | | `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) | | `styles` | нет | `{}` | Именованные стили | diff --git a/docs/mxl-guide.md b/docs/mxl-guide.md index 3663916c..a56df171 100644 --- a/docs/mxl-guide.md +++ b/docs/mxl-guide.md @@ -31,6 +31,22 @@ Claude напишет JSON-определение с областями, пар 3. `/mxl-validate` → проверка корректности 4. `/mxl-info` → верификация структуры (области, параметры) +### Создание макета по изображению + +Если есть скриншот или скан печатной формы, `/img-grid` поможет точно определить пропорции колонок. + +``` +> Вот изображение формы М-11. Создай макет по нему. +``` + +Рабочий цикл: +1. `/img-grid` → изображение с пронумерованной сеткой +2. Claude считает координаты границ колонок по сетке +3. Объединяет границы всех таблиц → базовая решётка +4. Пишет JSON DSL с пропорциями `"Nx"` и `"page": "A4-landscape"` +5. `/mxl-compile` автоматически вычисляет абсолютные ширины из пропорций и формата страницы +6. `/mxl-validate` → `/mxl-info` → проверка + ### Анализ существующего макета Быстрый обзор структуры макета без чтения тысяч строк XML.