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 <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-02-08 22:35:25 +03:00
parent 3819a5f7af
commit 044bc18974
7 changed files with 268 additions and 3 deletions
+77
View File
@@ -0,0 +1,77 @@
---
name: img-grid
description: Наложить пронумерованную сетку на изображение для определения пропорций колонок
argument-hint: <ImagePath> [-c COLS]
allowed-tools:
- Bash
- Read
---
# /img-grid — Сетка для анализа макета
Накладывает пронумерованную сетку на изображение печатной формы. Позволяет точно определить границы колонок, их пропорции и span-ы для генерации макета табличного документа.
## Использование
```
/img-grid <ImagePath> [-c COLS] [-o OUTPUT]
```
## Параметры
| Параметр | Обязательный | По умолчанию | Описание |
|-----------|:------------:|--------------|-----------------------------------------------|
| ImagePath | да | — | Путь к изображению (PNG, JPG) |
| -c COLS | нет | 50 | Количество вертикальных делений |
| -r ROWS | нет | авто | Количество горизонтальных делений (авто = квадратные ячейки) |
| -o OUTPUT | нет | `<name>-grid.<ext>` | Путь для результата |
## Команда
```bash
python .claude/skills/img-grid/scripts/overlay-grid.py "<ImagePath>" [-c 50] [-o "<OutputPath>"]
```
Требуется 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`
@@ -0,0 +1,112 @@
"""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
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: <name>-grid.<ext>)")
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()
+4 -1
View File
@@ -39,6 +39,8 @@ powershell.exe -NoProfile -File .claude/skills/mxl-compile/scripts/mxl-compile.p
3. Claude вызывает `/mxl-validate` для проверки корректности 3. Claude вызывает `/mxl-validate` для проверки корректности
4. Claude вызывает `/mxl-info` для верификации структуры 4. Claude вызывает `/mxl-info` для верификации структуры
**Если макет создаётся по изображению** (скриншот, скан печатной формы) — сначала вызвать `/img-grid` для наложения сетки, по ней определить границы колонок и пропорции, затем использовать `"Nx"` ширины + `"page"` для автоматического расчёта размеров.
## JSON-схема DSL ## JSON-схема DSL
Полная спецификация формата: **`docs/mxl-dsl-spec.md`** (прочитать через Read tool перед написанием JSON). Полная спецификация формата: **`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 } }, fonts: { name: { face, size, bold, italic, underline, strikeout } },
styles: { name: { font, align, valign, border, borderWidth, wrap, format } }, styles: { name: { font, align, valign, border, borderWidth, wrap, format } },
areas: [{ name, rows: [{ height, rowStyle, cells: [ 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 позиция колонки - `col` — 1-based позиция колонки
- `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине) - `rowStyle` — автозаполнение пустот стилем (рамки по всей ширине)
- Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template - Тип заполнения определяется автоматически: `param` → Parameter, `text` → Text, `template` → Template
@@ -119,6 +119,57 @@ function Parse-ColumnSpec {
return $cols 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 # Build column width map: 1-based col -> width
$colWidthMap = @{} $colWidthMap = @{}
if ($def.columnWidths) { if ($def.columnWidths) {
@@ -671,6 +722,9 @@ $enc = New-Object System.Text.UTF8Encoding($true)
# --- 9. Summary --- # --- 9. Summary ---
Write-Host "[OK] Compiled: $OutputPath" 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 " Areas: $($namedItems.Count), Rows: $totalRowCount, Columns: $totalColumns"
Write-Host " Fonts: $($fontEntries.Count), Lines: $lineCount, Formats: $($formatRegistry.Count)" Write-Host " Fonts: $($fontEntries.Count), Lines: $lineCount, Formats: $($formatRegistry.Count)"
Write-Host " Merges: $($merges.Count)" Write-Host " Merges: $($merges.Count)"
+3 -1
View File
@@ -21,6 +21,7 @@
|--------|--------|----------|------| |--------|--------|----------|------|
| Внешние обработки (EPF) | 10 навыков `/epf-*` | Создание, модификация, сборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) | | Внешние обработки (EPF) | 10 навыков `/epf-*` | Создание, модификация, сборка обработок из XML-исходников | [Подробнее](docs/epf-guide.md) |
| Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) | | Табличный документ (MXL) | 4 навыка `/mxl-*` | Анализ, создание, компиляция макетов печатных форм | [Подробнее](docs/mxl-guide.md) |
| Утилиты | `/img-grid` | Наложение сетки на изображение для определения пропорций колонок | — |
## Требования ## Требования
@@ -53,7 +54,8 @@
├── mxl-info/ # Анализ макета ├── mxl-info/ # Анализ макета
├── mxl-validate/ # Валидация макета ├── mxl-validate/ # Валидация макета
├── mxl-compile/ # Компиляция макета из JSON ├── mxl-compile/ # Компиляция макета из JSON
── mxl-decompile/ # Декомпиляция макета в JSON ── mxl-decompile/ # Декомпиляция макета в JSON
└── img-grid/ # Сетка для анализа изображений
docs/ docs/
├── epf-guide.md # Гайд: внешние обработки ├── epf-guide.md # Гайд: внешние обработки
├── mxl-guide.md # Гайд: табличный документ ├── mxl-guide.md # Гайд: табличный документ
+2 -1
View File
@@ -74,7 +74,8 @@
| Поле | Обяз. | По умолч. | Описание | | Поле | Обяз. | По умолч. | Описание |
|------|:-----:|-----------|----------| |------|:-----:|-----------|----------|
| `columns` | да | — | Количество колонок | | `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"`) | | `columnWidths` | нет | `{}` | Ширины колонок. Ключи 1-based: `"1"`, `"3-14"`, `"5,7,9"`. Значения: число (абсолют) или `"Nx"` (множитель от defaultWidth, напр. `"2x"`, `"0.5x"`) |
| `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) | | `fonts` | нет | — | Именованные шрифты (если не задано, создаётся Arial 10) |
| `styles` | нет | `{}` | Именованные стили | | `styles` | нет | `{}` | Именованные стили |
+16
View File
@@ -31,6 +31,22 @@ Claude напишет JSON-определение с областями, пар
3. `/mxl-validate` → проверка корректности 3. `/mxl-validate` → проверка корректности
4. `/mxl-info` → верификация структуры (области, параметры) 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. Быстрый обзор структуры макета без чтения тысяч строк XML.