diff --git a/.claude/skills/mxl-compile/SKILL.md b/.claude/skills/mxl-compile/SKILL.md index 2b0c5850..7e61a227 100644 --- a/.claude/skills/mxl-compile/SKILL.md +++ b/.claude/skills/mxl-compile/SKILL.md @@ -66,7 +66,10 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J columnGroups: [{ cols, name, collapsed, titleLocation }], header: { left, center, right, font, verticalAlignment, show, startPage }, footer: { … }, printSettings: { pageOrientation, topMargin, …, fitToPage, firstPageNumber }, - columnSets: { name: { columns, columnWidths, columnStyles } } + columnSets: { name: { columns, columnWidths, columnStyles } }, + pictures: { name: { ref } | { data, transparent, transparentPixel } | {} }, + drawings: [{ type, begin: { row, col, dy, dx }, end: { … }, picture, pictureSize, + text, name, detail, style, line, sides, id, zOrder }] } ``` @@ -87,6 +90,7 @@ powershell.exe -NoProfile -File "${CLAUDE_SKILL_DIR}/scripts/mxl-compile.ps1" -J - `rowGroups`/`columnGroups` — группы строк и колонок (сворачиваются кнопкой на полях): плоский список диапазонов, вложенность выражена вхождением одного в другой - `header`/`footer` — колонтитулы: три слота (`left`/`center`/`right`), общий шрифт и вертикальное выравнивание; текст может нести поля `[&НомерСтраницы]`, `[&Дата]` - `note` — всплывающая подсказка у ячейки: строка, объект языков или `{ text, style, autoSize, box }` +- `drawings` — рисунки поверх сетки (картинка, фигура, надпись): положение задают два якоря «ячейка + смещение в точках», картинка берётся по имени из `pictures` - `valueType` делает ячейку полем ввода (`"Number(15,3,nonneg)"`, `"String(10)"`, `"CatalogRef.Валюты"`, составной через ` + `); текста в такой ячейке быть не может, значение задаётся ключом `value` Двухуровневая шапка массивами: diff --git a/.claude/skills/mxl-compile/reference/dsl-spec.md b/.claude/skills/mxl-compile/reference/dsl-spec.md index ce6cd002..d4bbef9a 100644 --- a/.claude/skills/mxl-compile/reference/dsl-spec.md +++ b/.claude/skills/mxl-compile/reference/dsl-spec.md @@ -91,6 +91,8 @@ | `columnGroups` | нет | — | Группы колонок (см. ниже) | | `header` / `footer` | нет | — | Верхний и нижний колонтитулы (см. ниже) | | `printSettings` | нет | — | Параметры печати (см. ниже) | +| `pictures` | нет | — | Палитра картинок: ссылки на библиотеку платформы или данные (см. ниже) | +| `drawings` | нет | — | Рисунки: картинки, фигуры, надписи (см. ниже) | ## Области (`areas[]`) @@ -376,6 +378,54 @@ Диапазоны либо вложены, либо не пересекаются: частичное пересечение — ошибка. Порядок в списке не важен, число уровней вложенности считается само. +## Рисунки и картинки + +Рисунок — объект поверх сетки ячеек: картинка, фигура или надпись. Его положение задают два +якоря: ячейка плюс смещение в точках от её левого верхнего угла. + +```json +"pictures": { + "знак": { "ref": "v8ui:Стоп48" }, + "логотип": { "data": "iVBORw0KGgo...", "transparent": false } +}, +"drawings": [ + { "type": "Picture", "picture": "логотип", "name": "Логотип", + "begin": { "row": 1, "col": 1 }, + "end": { "row": 3, "col": 2, "dy": 34, "dx": 115 }, + "pictureSize": "Proportionally", "detail": "ПараметрРасшифровки" }, + { "type": "Rectangle", "style": "заливка", + "line": { "style": "Dashed", "width": 2 }, + "sides": { "left": true, "top": true, "right": false, "bottom": false }, + "begin": { "row": 5, "col": 1 }, "end": { "row": 6, "col": 3 } }, + { "type": "Text", "text": { "ru": "Подпись", "en": "Signature" }, + "begin": { "row": 8, "col": 1 }, "end": { "row": 8, "col": 3, "dy": 12 } } +] +``` + +| Поле | Обяз. | По умолч. | Описание | +|------|:-----:|-----------|----------| +| `type` | нет | `Picture` | `Picture`, `Rectangle`, `Ellipse`, `Line`, `Text`, `Chart`, `GanttChart` | +| `begin` / `end` | да | — | Якоря: `row`, `col` (1-based) и смещения `dy`, `dx` в точках | +| `picture` | нет | — | Имя записи из `pictures` | +| `pictureSize` | нет | `Stretch` | Как картинка заполняет прямоугольник: `Stretch`, `AutoSize`, `Proportionally`, `RealSize` | +| `text` | нет | — | Надпись: строка или объект «язык → текст» | +| `name` | нет | — | Имя рисунка, по которому к нему обращаются из кода | +| `detail` | нет | — | Параметр расшифровки | +| `style` | нет | — | Именованный стиль: заливка, шрифт, выравнивание надписи | +| `line` | нет | — | Линия рисунка (она же его рамка) — как `border` у ячейки | +| `sides` | нет | — | Какие стороны рамки видны: `left`, `top`, `right`, `bottom` | +| `id` | нет | номер по порядку | Идентификатор рисунка | +| `zOrder` | нет | номер по порядку | Порядок перекрытия: чем больше, тем выше | + +Порядок в списке `drawings` — порядок в документе; кто кого перекрывает, задаёт `zOrder`. + +Запись в `pictures` — либо ссылка на библиотеку картинок платформы (`ref`), либо сами данные +в base64 (`data`). Пустая запись `{}` — картинка не задана, такое в макетах встречается. +С данными сочетаются `transparent` (прозрачность включена) и `transparentPixel` +(`{ "x": …, "y": … }` — координата пикселя, чей цвет считается прозрачным). + +Одну запись `pictures` могут использовать несколько рисунков — данные в макете не дублируются. + ## `rowStyle` — оформление строки Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. @@ -391,7 +441,7 @@ round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попа XML не возвращается. - объединения, не привязанные к ячейке (по всей высоте или ширине документа); -- рисунки и картинки, в том числе штрихкоды; +- настройки диаграмм (`Chart`, `GanttChart`) — сам рисунок сохраняется, его содержимое нет; - область печати. Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому diff --git a/.claude/skills/mxl-compile/reference/format-properties.md b/.claude/skills/mxl-compile/reference/format-properties.md index 0c9a87b7..3c18ccf5 100644 --- a/.claude/skills/mxl-compile/reference/format-properties.md +++ b/.claude/skills/mxl-compile/reference/format-properties.md @@ -64,8 +64,6 @@ | `picHorizontalAlignment` | перечисление | `Auto`, `Center`, `Left`, `Right` | | `picVerticalAlignment` | перечисление | `Top`, `Center`, `Bottom` | | `textPosition` | перечисление | Положение текста относительно картинки: `Auto`, `Top`, `Right`, `Bottom` | -| `drawingBorder` | число | Рамка рисунка | -| `drawingHaveLeftBorder`, `drawingHaveTopBorder`, `drawingHaveRightBorder`, `drawingHaveBottomBorder` | да/нет | Наличие стороны рамки рисунка | ## Чего в стиле нет @@ -74,4 +72,6 @@ - `fillType` — выводится из того, каким ключом задано содержимое ячейки (`text` / `param` / `template`); - `containsValue`, `valueType`, `controlType` — свойства конкретной ячейки, а не общего - оформления: задаются ключами ячейки `valueType` и `controlType` (см. `dsl-spec.md`). + оформления: задаются ключами ячейки `valueType` и `controlType`; +- линия рисунка и её стороны — свойства рисунка: задаются его ключами `line` и `sides` + (у ячейки такого свойства нет вовсе) (см. `dsl-spec.md`). diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 index eee335e7..5d79c36f 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.ps1 @@ -1,4 +1,4 @@ -# mxl-compile v1.47 — Compile 1C spreadsheet from JSON +# mxl-compile v1.48 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -287,11 +287,15 @@ if (-not $hasDefault) { $script:lineRegistry = @() function Get-LineKey { + # Вид линии входит в ключ: одинаковые по стилю линии рамки ячейки и рисунка — РАЗНЫЕ записи, + # у них разный xsi:type. param($ln) - return "$($ln.Style)|$($ln.Width)|$($ln.Gap)" + return "$($ln.Style)|$($ln.Width)|$($ln.Gap)|$($ln.Kind)" } function Register-Line { + # Запись палитры линий типизирована: у рамки ячейки xsi:type Cell, у линии рисунка Drawing. + # Вид выводится из свойства, которое на линию ссылается, — отдельного ключа в DSL не нужно. param($ln) $key = Get-LineKey $ln for ($i = 0; $i -lt $script:lineRegistry.Count; $i++) { @@ -589,7 +593,11 @@ function Resolve-Style { continue } switch ($kinds[$tag]) { - 'line' { $props[$tag] = Register-Line (ConvertTo-LineValue $val $where) } + 'line' { + $lnVal = ConvertTo-LineValue $val $where + $lnVal['Kind'] = if ($tag -ceq 'drawingBorder') { 'Drawing' } else { 'Cell' } + $props[$tag] = Register-Line $lnVal + } 'bool' { $props[$tag] = if ($val -eq $true -or "$val" -eq 'true') { 'true' } else { 'false' } } 'int' { $props[$tag] = [int]$val } 'enum' { @@ -1023,15 +1031,15 @@ function Get-FormatMlTags { # line — ссылка в палитру ; color — #RRGGBB / style: / web: / win: # ml — многоязычная строка; enum — замкнутый список (см. Get-FormatEnumValues) # containsValue / valueType / controlType сюда НЕ входят: это свойства конкретной ячейки, -# а стиль — сущность общая, один на многие ячейки. +# а стиль — сущность общая, один на многие ячейки. По той же причине здесь нет линии рисунка +# (drawingBorder, drawingHave*Border): на корпусе все 2 135 записей с ними принадлежат только +# рисункам, у ячейки такого свойства не бывает — они задаются ключами самого рисунка. function Get-FormatTagKind { return @{ 'autoIndent' = 'int'; 'autoMarkIncomplete' = 'bool'; 'autoWidthCalculation' = 'bool' 'backColor' = 'color'; 'border' = 'line'; 'borderColor' = 'color' 'bottomBorder' = 'line'; 'bySelectedColumns' = 'bool'; 'columnSizeChange' = 'enum' - 'detailsUse' = 'enum'; 'drawingBorder' = 'int' - 'drawingHaveBottomBorder' = 'bool'; 'drawingHaveLeftBorder' = 'bool' - 'drawingHaveRightBorder' = 'bool'; 'drawingHaveTopBorder' = 'bool' + 'detailsUse' = 'enum' 'editFormat' = 'ml'; 'fillType' = 'enum'; 'font' = 'int'; 'format' = 'ml' 'height' = 'int'; 'hidden' = 'bool'; 'horizontalAlignment' = 'enum' 'hyperLink' = 'bool'; 'indent' = 'int'; 'leftBorder' = 'line' @@ -1582,6 +1590,100 @@ function Emit-PrintSettings { X "`t" } +# Палитра картинок. Ресурс хранится двумя способами: ссылкой на библиотеку платформы +# (ref="v8ui:Имя") либо данными base64 прямо в макете (9 905 из 9 988 картинок корпуса). +# На неё ссылаются и рисунки (pictureIndex), и ячейки (picIndex в стиле) — индекс 1-based. +$pictureNames = @{} # имя из DSL → 1-based индекс +$pictureEntries = @() + +if ($def.pictures) { + foreach ($pr in $def.pictures.PSObject.Properties) { + # Пустая запись — законная картинка «не задана»: в корпусе таких 151, ровно по одной + # на макет. Ошибкой считаем только запись с непонятным содержимым. + $entry = @{ Ref = ''; Data = ''; Transparent = ''; PixelX = ''; PixelY = '' } + if ($pr.Value.ref) { $entry.Ref = "$($pr.Value.ref)" } + if ($null -ne $pr.Value.data) { $entry.Data = "$($pr.Value.data)" } + if ($null -ne $pr.Value.transparent) { + $entry.Transparent = if ($pr.Value.transparent -eq $true -or "$($pr.Value.transparent)" -eq 'true') { 'true' } else { 'false' } + } + if ($null -ne $pr.Value.transparentPixel) { + $entry.PixelX = "$([int]$pr.Value.transparentPixel.x)" + $entry.PixelY = "$([int]$pr.Value.transparentPixel.y)" + } + if (-not $entry.Ref -and -not $entry.Data -and + ($entry.Transparent -or $entry.PixelX)) { + [Console]::Error.WriteLine("pictures[$($pr.Name)]: 'transparent' and 'transparentPixel' require 'data'") + exit 1 + } + $pictureEntries += $entry + $pictureNames[$pr.Name] = $pictureEntries.Count + } +} + +# Рисунки: своя геометрия из двух якорей, тип, ссылка на картинку, текст, расшифровка. +# id и zOrder — данные, а не позиция: на корпусе они расходятся у 8 253 рисунков из 11 268. +$drawingTypes = @('Picture', 'Rectangle', 'Ellipse', 'Line', 'Text', 'Chart', 'GanttChart') +$drawings = @() +$drawingIdx = 0 +foreach ($dr in $def.drawings) { + $drawingIdx++ + $where = "drawings[$drawingIdx]" + $type = if ($dr.type) { "$($dr.type)" } else { 'Picture' } + $canonType = @($drawingTypes | Where-Object { $_ -eq $type })[0] + if (-not $canonType) { + [Console]::Error.WriteLine("${where}: unknown drawing 'type' `"$type`". Allowed: $($drawingTypes -join ', ')") + exit 1 + } + $picIdx = 0 + if ($dr.picture) { + if (-not $pictureNames.ContainsKey("$($dr.picture)")) { + [Console]::Error.WriteLine("${where}: unknown 'picture' `"$($dr.picture)`" — not declared in pictures") + exit 1 + } + $picIdx = $pictureNames["$($dr.picture)"] + } + # Оформление рисунка: общее — из именованного стиля, чисто рисуночное (линия и её стороны) + # — своими ключами, поверх стиля. Тот же приём, что у ячейки с fillType и значением. + $props = @{} + if ($dr.style) { $props = Resolve-Style -styleName "$($dr.style)" -fillType "" } + if ($null -ne $dr.line) { + $lnVal = ConvertTo-LineValue $dr.line $where + $lnVal['Kind'] = 'Drawing' + $props['drawingBorder'] = Register-Line $lnVal + } + foreach ($side in @('left', 'top', 'right', 'bottom')) { + $v = $dr.sides.$side + if ($null -ne $v) { + $tag = 'drawingHave' + $side.Substring(0,1).ToUpperInvariant() + $side.Substring(1) + 'Border' + $props[$tag] = if ($v -eq $true -or "$v" -eq 'true') { 'true' } else { 'false' } + } + } + $fmtIdx = 0 + if ($props.Count -gt 0) { $fmtIdx = Register-Format $props } + function Get-Anchor { + param($node, [string]$rowKey, [string]$colKey) + return @{ + Row = if ($node -and $null -ne $node.row) { [int]$node.row - 1 } else { 0 } + Col = if ($node -and $null -ne $node.col) { [int]$node.col - 1 } else { 0 } + Dy = if ($node -and $null -ne $node.dy) { [int]$node.dy } else { 0 } + Dx = if ($node -and $null -ne $node.dx) { [int]$node.dx } else { 0 } + } + } + $drawings += @{ + Type = $canonType + Id = if ($null -ne $dr.id) { [int]$dr.id } else { $drawingIdx } + ZOrder = if ($null -ne $dr.zOrder) { [int]$dr.zOrder } else { $drawingIdx } + FormatIdx = $fmtIdx + Detail = $dr.detail + Text = $dr.text + Begin = Get-Anchor $dr.begin + End = Get-Anchor $dr.end + PictureSize = if ($dr.pictureSize) { "$($dr.pictureSize)" } else { 'Stretch' } + PictureIdx = $picIdx + Name = $dr.name + } +} + # Формат по умолчанию — последняя запись палитры (см. выше). $defaultFormatIndex = Register-Format @{ width = $defaultWidth } @@ -2173,6 +2275,47 @@ if ($def.namedAreas) { } } +# 7d-bis2. Рисунки идут сразу после строк, до колонтитулов. Порядок тегов снят с корпуса: +# у всех 11 268 рисунков он один и тот же. +foreach ($dr in $drawings) { + X "`t" + X "`t`t$($dr.Type)" + X "`t`t$($dr.Id)" + X "`t`t$($dr.FormatIdx)" + if ($dr.Detail) { X "`t`t$($dr.Detail)" } + if ($null -ne $dr.Text) { + $pairs = @() + if ($dr.Text -is [System.Management.Automation.PSCustomObject]) { + foreach ($pr in $dr.Text.PSObject.Properties) { $pairs += @{ Lang = $pr.Name; Text = "$($pr.Value)" } } + } else { + foreach ($l in $textLanguages) { $pairs += @{ Lang = $l; Text = "$($dr.Text)" } } + } + X "`t`t" + foreach ($pr in $pairs) { + X "`t`t`t" + X "`t`t`t`t$($pr.Lang)" + X "`t`t`t`t$(Esc-XmlText $pr.Text)" + X "`t`t`t" + } + X "`t`t" + } + X "`t`t$($dr.Begin.Row)" + X "`t`t$($dr.Begin.Dy)" + X "`t`t$($dr.End.Row)" + X "`t`t$($dr.End.Dy)" + X "`t`t$($dr.Begin.Col)" + X "`t`t$($dr.Begin.Dx)" + X "`t`t$($dr.End.Col)" + X "`t`t$($dr.End.Dx)" + # autoSize у рисунка на корпусе всегда false (11 268 из 11 268), «АвтоРазмер» из + # диалога уходит в pictureSize. + X "`t`tfalse" + X "`t`t$($dr.PictureSize)" + X "`t`t$($dr.ZOrder)" + if ($dr.PictureIdx -gt 0) { X "`t`t$($dr.PictureIdx)" } + X "`t" +} + # 7d-ter. Колонтитулы: шесть слотов идут после строк и перед скалярными свойствами документа. foreach ($slot in $script:headerSlots) { if ($def.header) { Emit-HeaderSlot "$($slot)Header" $def.header.$slot $headerFmt } @@ -2294,9 +2437,15 @@ $colGroups = Get-Groups 'columnGroups' 'cols' # 7e. Scalar metadata X "`ttrue" X "`t$defaultFormatIndex" -X "`t$totalRowCount" +# Рисунок расширяет документ, не создавая строк: на стендах высота равна нижней границе +# самого нижнего рисунка, при том что строк записана всего одна. +$docHeight = $totalRowCount +foreach ($dr in $drawings) { + if (($dr.End.Row + 1) -gt $docHeight) { $docHeight = $dr.End.Row + 1 } +} +X "`t$docHeight" if ($rowGroups.Count -gt 0) { X "`t$(Get-GroupLevels $rowGroups)" } -X "`t$totalRowCount" +X "`t$docHeight" Emit-Groups 'vg' $rowGroups Emit-Groups 'hg' $colGroups @@ -2324,8 +2473,20 @@ function Get-OrdinalKey { param([string]$name) return (($name.ToLowerInvariant().ToCharArray() | ForEach-Object { '{0:X4}' -f [int]$_ }) -join '') } +# Имя рисунка — namedItem другого типа, но список общий: на корпусе оба вида отсортированы +# вместе по имени во всех 266 макетах с именованными рисунками. +foreach ($dr in $drawings) { + if ($dr.Name) { $namedItems += @{ Name = "$($dr.Name)"; DrawingId = $dr.Id } } +} $sortedNamedItems = @($namedItems | Sort-Object -Property @{ Expression = { Get-OrdinalKey $_.Name } }) foreach ($ni in $sortedNamedItems) { + if ($ni.DrawingId) { + X "`t" + X "`t`t$($ni.Name)" + X "`t`t$($ni.DrawingId)" + X "`t" + continue + } # Тип области выводится из указанных осей, как в ТабличныйДокумент.Область(): # нет колонок → полоса строк, нет строк → полоса колонок, обе → прямоугольник. $hasRows = $ni.BeginRow -ge 0 @@ -2347,7 +2508,8 @@ foreach ($ni in $sortedNamedItems) { # 7h. Line palette foreach ($ln in $script:lineRegistry) { X "`t" - X "`t`t$($ln.Style)" + $lineKind = if ($ln.Kind -ceq 'Drawing') { 'SpreadsheetDocumentDrawingLineType' } else { 'SpreadsheetDocumentCellLineType' } + X "`t`t$($ln.Style)" X "`t" } @@ -2404,7 +2566,31 @@ foreach ($key in $formatRegistry.Keys) { X "`t" } -# 7k. Close document +# 7k. Палитра картинок — последний блок документа. Индекс здесь 0-based, а ссылки +# на него (pictureIndex рисунка, picIndex стиля) 1-based. +$picIdx = 0 +foreach ($pic in $pictureEntries) { + X "`t" + X "`t`t$picIdx" + if ($pic.Ref) { + X "`t`t" + } elseif (-not $pic.Data) { + X "`t`t" + } else { + $attr = if ($pic.Transparent) { " t=`"$($pic.Transparent)`"" } else { '' } + if ($pic.PixelX) { $attr += " tx=`"$($pic.PixelX)`" ty=`"$($pic.PixelY)`"" } + # Данные платформа переносит по строкам тем же переводом, что и весь файл, — + # в отличие от блоба настроек элемента управления, где перенос голый LF. + $parts = @("$($pic.Data)" -split "`r?`n") + $parts[0] = "`t`t$($parts[0])" + $parts[-1] = "$($parts[-1])" + foreach ($line in $parts) { X $line } + } + X "`t" + $picIdx++ +} + +# 7l. Close document X '' # --- 8. Write output --- diff --git a/.claude/skills/mxl-compile/scripts/mxl-compile.py b/.claude/skills/mxl-compile/scripts/mxl-compile.py index a391eac3..5bc3feea 100644 --- a/.claude/skills/mxl-compile/scripts/mxl-compile.py +++ b/.claude/skills/mxl-compile/scripts/mxl-compile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-compile v1.47 — Compile 1C spreadsheet from JSON +# mxl-compile v1.48 — Compile 1C spreadsheet from JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse import hashlib @@ -355,14 +355,14 @@ def format_tag_kind(): line — ссылка в палитру ; color — #RRGGBB / style: / web: / win: ml — многоязычная строка; enum — замкнутый список (см. format_enum_values). containsValue / valueType / controlType сюда НЕ входят: это свойства конкретной ячейки, - а стиль — сущность общая, один на многие ячейки.""" + а стиль — сущность общая, один на многие ячейки. По той же причине здесь нет линии рисунка + (drawingBorder, drawingHave*Border): на корпусе все 2 135 записей с ними принадлежат только + рисункам, у ячейки такого свойства не бывает — они задаются ключами самого рисунка.""" return { 'autoIndent': 'int', 'autoMarkIncomplete': 'bool', 'autoWidthCalculation': 'bool', 'backColor': 'color', 'border': 'line', 'borderColor': 'color', 'bottomBorder': 'line', 'bySelectedColumns': 'bool', 'columnSizeChange': 'enum', - 'detailsUse': 'enum', 'drawingBorder': 'int', - 'drawingHaveBottomBorder': 'bool', 'drawingHaveLeftBorder': 'bool', - 'drawingHaveRightBorder': 'bool', 'drawingHaveTopBorder': 'bool', + 'detailsUse': 'enum', 'editFormat': 'ml', 'fillType': 'enum', 'font': 'int', 'format': 'ml', 'height': 'int', 'hidden': 'bool', 'horizontalAlignment': 'enum', 'hyperLink': 'bool', 'indent': 'int', 'leftBorder': 'line', @@ -382,8 +382,8 @@ def format_enum_values(): 'detailsUse': ['Cell', 'Row', 'WithoutProcessing'], 'fillType': ['Parameter', 'Template', 'Text'], 'horizontalAlignment': ['Auto', 'Center', 'Justify', 'Left', 'Right'], - 'pattern': ['Pattern7', 'Pattern10', 'Pattern12', 'Pattern13', 'Pattern14', - 'Pattern16', 'Solid', 'WithoutPattern'], + # Узоры платформа нумерует подряд, без семантических имён: «Узор 1» … «Узор 17». + 'pattern': ['WithoutPattern', 'Solid', 'Pattern1', 'Pattern2', 'Pattern3', 'Pattern4', 'Pattern5', 'Pattern6', 'Pattern7', 'Pattern8', 'Pattern9', 'Pattern10', 'Pattern11', 'Pattern12', 'Pattern13', 'Pattern14', 'Pattern15', 'Pattern16', 'Pattern17'], 'picHorizontalAlignment': ['Auto', 'Center', 'Left', 'Right'], 'picVerticalAlignment': ['Bottom', 'Center', 'Top'], 'pictureSizeMode': ['AutoSize', 'Proportionally', 'RealSize'], @@ -872,9 +872,12 @@ def main(): line_registry = [] def register_line(ln): - key = (ln['Style'], str(ln['Width']), ln['Gap']) + # Запись палитры линий типизирована: у рамки ячейки xsi:type Cell, у линии рисунка + # Drawing. Вид входит в ключ, иначе одинаковые по стилю линии двух видов слились бы. + key = (ln['Style'], str(ln['Width']), ln['Gap'], ln.get('Kind', 'Cell')) for i, existing in enumerate(line_registry): - if (existing['Style'], str(existing['Width']), existing['Gap']) == key: + if (existing['Style'], str(existing['Width']), existing['Gap'], + existing.get('Kind', 'Cell')) == key: return i line_registry.append(ln) return len(line_registry) - 1 @@ -1122,7 +1125,9 @@ def main(): continue kind = kinds[tag] if kind == 'line': - props[tag] = register_line(to_line_value(val, where)) + ln_val = to_line_value(val, where) + ln_val['Kind'] = 'Drawing' if tag == 'drawingBorder' else 'Cell' + props[tag] = register_line(ln_val) elif kind == 'bool': props[tag] = 'true' if (val is True or str(val) == 'true') else 'false' elif kind == 'int': @@ -1542,6 +1547,87 @@ def main(): lines.extend(emitted) lines.append('\t') + # Палитра картинок. Ресурс хранится двумя способами: ссылкой на библиотеку платформы + # (ref="v8ui:Имя") либо данными base64 прямо в макете (9 905 из 9 988 картинок корпуса). + # На неё ссылаются и рисунки (pictureIndex), и ячейки (picIndex в стиле) — индекс 1-based. + picture_names = {} + picture_entries = [] + for pic_name, pic_def in (defn.get('pictures') or {}).items(): + # Пустая запись — законная картинка «не задана»: в корпусе таких 151, ровно по одной + # на макет. Ошибкой считаем только запись с непонятным содержимым. + entry = {'Ref': '', 'Data': '', 'Transparent': '', 'PixelX': '', 'PixelY': ''} + if pic_def.get('ref'): + entry['Ref'] = str(pic_def['ref']) + if pic_def.get('data') is not None: + entry['Data'] = str(pic_def['data']) + if pic_def.get('transparent') is not None: + entry['Transparent'] = 'true' if (pic_def['transparent'] is True + or str(pic_def['transparent']).lower() == 'true') else 'false' + if pic_def.get('transparentPixel') is not None: + entry['PixelX'] = str(int(pic_def['transparentPixel'].get('x', 0))) + entry['PixelY'] = str(int(pic_def['transparentPixel'].get('y', 0))) + if not entry['Ref'] and not entry['Data'] and (entry['Transparent'] or entry['PixelX']): + print(f"pictures[{pic_name}]: 'transparent' and 'transparentPixel' require 'data'", file=sys.stderr) + sys.exit(1) + picture_entries.append(entry) + picture_names[pic_name] = len(picture_entries) + + # Рисунки: своя геометрия из двух якорей, тип, ссылка на картинку, текст, расшифровка. + # id и zOrder — данные, а не позиция: на корпусе они расходятся у 8 253 рисунков из 11 268. + DRAWING_TYPES = ('Picture', 'Rectangle', 'Ellipse', 'Line', 'Text', 'Chart', 'GanttChart') + + def drawing_anchor(node): + node = node or {} + return { + 'Row': int(node['row']) - 1 if node.get('row') is not None else 0, + 'Col': int(node['col']) - 1 if node.get('col') is not None else 0, + 'Dy': int(node['dy']) if node.get('dy') is not None else 0, + 'Dx': int(node['dx']) if node.get('dx') is not None else 0, + } + + drawings = [] + for idx, dr in enumerate(defn.get('drawings') or [], start=1): + where = f'drawings[{idx}]' + dtype = str(dr.get('type') or 'Picture') + canon_type = next((t for t in DRAWING_TYPES if t.lower() == dtype.lower()), None) + if canon_type is None: + print(f'{where}: unknown drawing \'type\' "{dtype}". Allowed: {", ".join(DRAWING_TYPES)}', + file=sys.stderr) + sys.exit(1) + pic_idx = 0 + if dr.get('picture'): + if str(dr['picture']) not in picture_names: + print(f'{where}: unknown \'picture\' "{dr["picture"]}" — not declared in pictures', + file=sys.stderr) + sys.exit(1) + pic_idx = picture_names[str(dr['picture'])] + # Оформление рисунка: общее — из именованного стиля, чисто рисуночное (линия и её + # стороны) — своими ключами, поверх стиля. Тот же приём, что у ячейки с fillType. + props = resolve_style(str(dr['style']), '') if dr.get('style') else {} + if dr.get('line') is not None: + ln_val = to_line_value(dr['line'], where) + ln_val['Kind'] = 'Drawing' + props['drawingBorder'] = register_line(ln_val) + for side in ('left', 'top', 'right', 'bottom'): + v = (dr.get('sides') or {}).get(side) + if v is not None: + tag = 'drawingHave' + side.capitalize() + 'Border' + props[tag] = 'true' if (v is True or str(v).lower() == 'true') else 'false' + fmt_idx = register_format(props) if props else 0 + drawings.append({ + 'Type': canon_type, + 'Id': int(dr['id']) if dr.get('id') is not None else idx, + 'ZOrder': int(dr['zOrder']) if dr.get('zOrder') is not None else idx, + 'FormatIdx': fmt_idx, + 'Detail': dr.get('detail'), + 'Text': dr.get('text'), + 'Begin': drawing_anchor(dr.get('begin')), + 'End': drawing_anchor(dr.get('end')), + 'PictureSize': str(dr.get('pictureSize') or 'Stretch'), + 'PictureIdx': pic_idx, + 'Name': dr.get('name'), + }) + # Формат по умолчанию — последняя запись палитры (см. выше). default_format_index = register_format({'width': default_width}) @@ -2065,6 +2151,45 @@ def main(): 'ColumnSet': na_set, }) + # 7d-bis2. Рисунки идут сразу после строк, до колонтитулов. Порядок тегов снят с корпуса: + # у всех 11 268 рисунков он один и тот же. + for dr in drawings: + lines.append('\t') + lines.append(f'\t\t{dr["Type"]}') + lines.append(f'\t\t{dr["Id"]}') + lines.append(f'\t\t{dr["FormatIdx"]}') + if dr['Detail']: + lines.append(f'\t\t{dr["Detail"]}') + if dr['Text'] is not None: + value = dr['Text'] + if isinstance(value, dict): + pairs = [(str(k), str(v)) for k, v in value.items()] + else: + pairs = [(lang, str(value)) for lang in text_languages] + lines.append('\t\t') + for lang, content in pairs: + lines.append('\t\t\t') + lines.append(f'\t\t\t\t{lang}') + lines.append(f'\t\t\t\t{esc_xml_text(content)}') + lines.append('\t\t\t') + lines.append('\t\t') + lines.append(f'\t\t{dr["Begin"]["Row"]}') + lines.append(f'\t\t{dr["Begin"]["Dy"]}') + lines.append(f'\t\t{dr["End"]["Row"]}') + lines.append(f'\t\t{dr["End"]["Dy"]}') + lines.append(f'\t\t{dr["Begin"]["Col"]}') + lines.append(f'\t\t{dr["Begin"]["Dx"]}') + lines.append(f'\t\t{dr["End"]["Col"]}') + lines.append(f'\t\t{dr["End"]["Dx"]}') + # autoSize у рисунка на корпусе всегда false (11 268 из 11 268), «АвтоРазмер» из + # диалога уходит в pictureSize. + lines.append('\t\tfalse') + lines.append(f'\t\t{dr["PictureSize"]}') + lines.append(f'\t\t{dr["ZOrder"]}') + if dr['PictureIdx'] > 0: + lines.append(f'\t\t{dr["PictureIdx"]}') + lines.append('\t') + # 7d-ter. Колонтитулы: шесть слотов идут после строк и перед скалярными свойствами документа. for slot in ('left', 'center', 'right'): if defn.get('header'): @@ -2176,10 +2301,15 @@ def main(): # 7e. Scalar metadata lines.append(f'\ttrue') lines.append(f'\t{default_format_index}') - lines.append(f'\t{total_row_count}') + # Рисунок расширяет документ, не создавая строк: на стендах высота равна нижней границе + # самого нижнего рисунка, при том что строк записана всего одна. + doc_height = total_row_count + for dr in drawings: + doc_height = max(doc_height, dr['End']['Row'] + 1) + lines.append(f'\t{doc_height}') if row_groups: lines.append(f'\t{group_levels(row_groups)}') - lines.append(f'\t{total_row_count}') + lines.append(f'\t{doc_height}') emit_groups(lines, 'vg', row_groups) emit_groups(lines, 'hg', col_groups) @@ -2200,7 +2330,19 @@ def main(): # с несколькими элементами иного порядка нет ни разу. Сортировка регистронезависимая и # ординальная (в ps1 поэтому нельзя Sort-Object — он сортирует по текущей культуре). # NB: имён с «ё» в выборке не встретилось, этот случай не проверен. + # Имя рисунка — namedItem другого типа, но список общий: на корпусе оба вида отсортированы + # вместе по имени во всех 266 макетах с именованными рисунками. + for dr in drawings: + if dr['Name']: + named_items.append({'Name': str(dr['Name']), 'DrawingId': dr['Id']}) + for ni in sorted(named_items, key=lambda x: str(x['Name']).lower()): + if ni.get('DrawingId'): + lines.append('\t') + lines.append(f'\t\t{ni["Name"]}') + lines.append(f'\t\t{ni["DrawingId"]}') + lines.append('\t') + continue # Тип области выводится из указанных осей, как в ТабличныйДокумент.Область(): # нет колонок → полоса строк, нет строк → полоса колонок, обе → прямоугольник. has_rows = int(ni['BeginRow']) >= 0 @@ -2222,7 +2364,9 @@ def main(): # 7h. Line palette for ln in line_registry: lines.append(f'\t') - lines.append(f'\t\t{ln["Style"]}') + line_kind = ('SpreadsheetDocumentDrawingLineType' if ln.get('Kind') == 'Drawing' + else 'SpreadsheetDocumentCellLineType') + lines.append(f'\t\t{ln["Style"]}') lines.append('\t') # 7i. Font palette @@ -2275,7 +2419,28 @@ def main(): lines.append('\t') - # 7k. Close document + # 7k. Палитра картинок — последний блок документа. Индекс здесь 0-based, а ссылки + # на него (pictureIndex рисунка, picIndex стиля) 1-based. + for pic_i, pic in enumerate(picture_entries): + lines.append('\t') + lines.append(f'\t\t{pic_i}') + if pic['Ref']: + lines.append(f'\t\t') + elif not pic['Data']: + lines.append('\t\t') + else: + attr = f' t="{pic["Transparent"]}"' if pic['Transparent'] else '' + if pic['PixelX']: + attr += f' tx="{pic["PixelX"]}" ty="{pic["PixelY"]}"' + # Данные платформа переносит по строкам тем же переводом, что и весь файл, — + # в отличие от блоба настроек элемента управления, где перенос голый LF. + parts = str(pic['Data']).replace('\r\n', '\n').split('\n') + parts[0] = f'\t\t{parts[0]}' + parts[-1] = f'{parts[-1]}' + lines.extend(parts) + lines.append('\t') + + # 7l. Close document lines.append('') # --- 8. Write output --- diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 index dd849f5d..6db6495f 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.ps1 @@ -1,4 +1,4 @@ -# mxl-decompile v1.26 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.27 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Parameter(Mandatory)] @@ -41,7 +41,7 @@ function Get-FormatTagKind { 'autoIndent' = 'int'; 'autoMarkIncomplete' = 'bool'; 'autoWidthCalculation' = 'bool' 'backColor' = 'color'; 'border' = 'line'; 'borderColor' = 'color' 'bottomBorder' = 'line'; 'bySelectedColumns' = 'bool'; 'columnSizeChange' = 'enum' - 'detailsUse' = 'enum'; 'drawingBorder' = 'int' + 'detailsUse' = 'enum'; 'drawingBorder' = 'line' 'drawingHaveBottomBorder' = 'bool'; 'drawingHaveLeftBorder' = 'bool' 'drawingHaveRightBorder' = 'bool'; 'drawingHaveTopBorder' = 'bool' 'editFormat' = 'ml'; 'fillType' = 'enum'; 'font' = 'int'; 'format' = 'ml' @@ -265,6 +265,98 @@ function Get-Format { return $rawFormats[$idx - 1] } +# --- 4-bis. Палитра картинок и рисунки --- +# Картинка хранится ссылкой на библиотеку либо данными base64; ссылки на палитру (у рисунка +# pictureIndex, у ячейки picIndex) — 1-based, сам — 0-based. +$picturesOut = [ordered]@{} +$pictureKey = @{} +$picI = 0 +foreach ($picNode in $root.SelectNodes("d:picture", $ns)) { + $inner = $picNode.SelectSingleNode("d:picture", $ns) + if (-not $inner) { continue } + $picI++ + $name = "pic$picI" + $entry = [ordered]@{} + if ($inner.GetAttribute('ref')) { + $entry["ref"] = $inner.GetAttribute('ref') + } elseif (($inner.InnerText).Trim()) { + # System.Xml переводы строк в тексте не нормализует, а lxml нормализует: без этой + # замены порты дали бы разный JSON на одном и том же файле. + $entry["data"] = ($inner.InnerText -replace "`r`n", "`n").Trim() + if ($inner.HasAttribute('t')) { $entry["transparent"] = ($inner.GetAttribute('t') -ceq 'true') } + # Пиксель прозрачного цвета: координата внутри самой картинки (проверено — + # это не её размеры). В корпусе встречается без атрибута t. + if ($inner.HasAttribute('tx')) { + $entry["transparentPixel"] = [ordered]@{ + x = [int]$inner.GetAttribute('tx') + y = [int]$inner.GetAttribute('ty') + } + } + } + $picturesOut[$name] = $entry + $pictureKey[$picI] = $name +} + +$drawingNames = @{} +foreach ($niNode in $root.SelectNodes("d:namedItem", $ns)) { + if ($niNode.GetAttribute("type", "http://www.w3.org/2001/XMLSchema-instance") -cne 'NamedItemDrawing') { continue } + $idN = $niNode.SelectSingleNode("d:drawingID", $ns) + $nmN = $niNode.SelectSingleNode("d:name", $ns) + $drawingNames[$(if ($idN) { $idN.InnerText } else { '' })] = $(if ($nmN) { $nmN.InnerText } else { '' }) +} + +$drawingsOut = @() +$drI = 0 +foreach ($dr in $root.SelectNodes("d:drawing", $ns)) { + $drI++ + $item = [ordered]@{} + $tN = $dr.SelectSingleNode("d:drawingType", $ns) + $item["type"] = if ($tN) { $tN.InnerText } else { 'Picture' } + $idN = $dr.SelectSingleNode("d:id", $ns) + $did = if ($idN) { $idN.InnerText } else { "$drI" } + if ($did -cne "$drI") { $item["id"] = [int]$did } + $zN = $dr.SelectSingleNode("d:zOrder", $ns) + $zorder = if ($zN) { $zN.InnerText } else { "$drI" } + if ($zorder -cne "$drI") { $item["zOrder"] = [int]$zorder } + if ($drawingNames.ContainsKey($did)) { $item["name"] = $drawingNames[$did] } + $piN = $dr.SelectSingleNode("d:pictureIndex", $ns) + if ($piN -and $pictureKey.ContainsKey([int]$piN.InnerText)) { + $item["picture"] = $pictureKey[[int]$piN.InnerText] + } + $dpN = $dr.SelectSingleNode("d:detailParameter", $ns) + if ($dpN -and $dpN.InnerText) { $item["detail"] = $dpN.InnerText } + $tEl = $dr.SelectSingleNode("d:text", $ns) + if ($tEl) { + $byLang = [ordered]@{} + foreach ($it in $tEl.SelectNodes("v8:item", $ns)) { + $l = $it.SelectSingleNode("v8:lang", $ns) + $cnt = $it.SelectSingleNode("v8:content", $ns) + $byLang[$(if ($l) { $l.InnerText } else { '' })] = $(if ($cnt) { $cnt.InnerText } else { '' }) + } + $item["text"] = $byLang + } + # Оформление разбирается позже: линию нужно развернуть в описание, а помощник для + # этого появляется вместе с палитрой стилей. + $fN = $dr.SelectSingleNode("d:formatIndex", $ns) + foreach ($pair in @(@('begin', 'beginRowOffset', 'beginColumnOffset'), @('end', 'endRowOffset', 'endColumnOffset'))) { + $prefix = $pair[0] + $rN = $dr.SelectSingleNode("d:$($prefix)Row", $ns) + $cN = $dr.SelectSingleNode("d:$($prefix)Column", $ns) + $dyN = $dr.SelectSingleNode("d:$($pair[1])", $ns) + $dxN = $dr.SelectSingleNode("d:$($pair[2])", $ns) + $item[$prefix] = [ordered]@{ + row = $(if ($rN) { [int]$rN.InnerText } else { 0 }) + 1 + col = $(if ($cN) { [int]$cN.InnerText } else { 0 }) + 1 + dy = $(if ($dyN) { [int]$dyN.InnerText } else { 0 }) + dx = $(if ($dxN) { [int]$dxN.InnerText } else { 0 }) + } + } + $psN = $dr.SelectSingleNode("d:pictureSize", $ns) + if ($psN -and $psN.InnerText -cne 'Stretch') { $item["pictureSize"] = $psN.InnerText } + $item["_fmt"] = if ($fN) { [int]$fN.InnerText } else { 0 } + $drawingsOut += , $item +} + # --- 4a. Колонтитулы и параметры печати --- # Слот колонтитула устроен как ячейка: ссылка на формат плюс текст. Признак вывода и стартовую # страницу читаем только у записи, где есть : палитра дедуплицирована, и колонтитул без @@ -640,7 +732,11 @@ function Get-DslText { # общий на многие ячейки, а они индивидуальны. Компилятор таких ключей не знает, так что # в DSL они были чистым шумом. $nonStyleTags = @('width', 'height', 'fillType', 'font', - 'containsValue', 'valueType', 'controlType') + 'containsValue', 'valueType', 'controlType', + # Линия рисунка и её стороны — свойства рисунка, а не общего оформления: + # на корпусе все 2 135 записей с ними принадлежат только рисункам. + 'drawingBorder', 'drawingHaveLeftBorder', 'drawingHaveTopBorder', + 'drawingHaveRightBorder', 'drawingHaveBottomBorder') $borderTags = @('border', 'leftBorder', 'topBorder', 'rightBorder', 'bottomBorder') function ConvertTo-DslLine { @@ -793,6 +889,20 @@ foreach ($r in ($rowData.Keys | Sort-Object { [int]$_ } | ForEach-Object { $rowD } } +# Формат рисунка — ещё один владелец записи палитры, наравне с ячейкой, строкой, колонкой +# и примечанием: без этого оформление рисунка теряло имя и вырезалось как неиспользуемое. +foreach ($item in $drawingsOut) { + $fi = [int]$item['_fmt'] + if ($fi -le 0) { continue } + $dfmt = Get-Format $fi + if (-not $dfmt) { continue } + if ((Get-StyleProps $dfmt).Count -gt 0 -or $dfmt.FontIdx -ge 0) { + $dkey = Get-StyleKey $dfmt + if (-not $styleKeys.Contains($dkey)) { $styleKeys[$dkey] = $dfmt } + $formatToStyleKey[$fi] = $dkey + } +} + # Оформление строки без её собственных свойств: скрытие уезжает инлайном к height, # поэтому в именованный стиль попадать не должно. function Get-RowStyleFmt { @@ -1519,6 +1629,15 @@ foreach ($cs in $columnSets) { if ($name) { $usedStyles[$name] = $true } } } +# Рисунок — пятый владелец формата. Имя ему присваивается позже, при сборке результата, +# поэтому здесь считаем его из ссылки на палитру: иначе стиль рисунка вырежется как +# неиспользуемый, а ссылка на него останется висячей. +foreach ($item in $drawingsOut) { + $fi = [int]$item['_fmt'] + if ($fi -le 0) { continue } + $dname = Get-StyleName $fi + if ($dname -cne 'default') { $usedStyles[$dname] = $true } +} $toRemove = @($styleDefs.Keys | Where-Object { -not $usedStyles.ContainsKey($_) }) foreach ($s in $toRemove) { $styleDefs.Remove($s) } @@ -1553,10 +1672,44 @@ if ($colGroups.Count -gt 0) { $result["columnGroups"] = [array]$colGroups } # становится известен только после разбора строк. $headerPart = Read-HeaderPart 'Header' $footerPart = Read-HeaderPart 'Footer' -if ($headerPart) { $result["header"] = $headerPart } -if ($footerPart) { $result["footer"] = $footerPart } +# Пустой словарь в PowerShell истинен, поэтому проверяем именно наполнение: макет со ссылкой +# на общую запись палитры, но без своих настроек колонтитула, иначе получал в DSL пустой ключ. +if ($headerPart -and $headerPart.Count -gt 0) { $result["header"] = $headerPart } +if ($footerPart -and $footerPart.Count -gt 0) { $result["footer"] = $footerPart } if ($printSettings) { $result["printSettings"] = $printSettings } +# Оформление рисунка: линия и её стороны — его собственные ключи, общее — именованный стиль. +# Порядок ключей задаём здесь же, чтобы он не зависел от того, что разбиралось раньше. +$drawingsOrdered = @() +foreach ($item in $drawingsOut) { + $fmtIdx = [int]$item['_fmt'] + $item.Remove('_fmt') + if ($item.Contains("text")) { $item["text"] = Get-DslText $item["text"] } + $lineVal = $null + $sides = [ordered]@{} + if ($fmtIdx -gt 0) { + $dfmt = Get-Format $fmtIdx + $props = if ($dfmt) { $dfmt.Props } else { @{} } + if ($props.Contains('drawingBorder')) { $lineVal = ConvertTo-DslLine ([int]$props['drawingBorder']) } + foreach ($side in @('Left', 'Top', 'Right', 'Bottom')) { + $tag = "drawingHave${side}Border" + if ($props.Contains($tag)) { $sides[$side.ToLowerInvariant()] = ($props[$tag] -ceq 'true') } + } + $dname = Get-StyleName $fmtIdx + if ($dname -cne 'default') { $item["style"] = $dname } + } + if ($null -ne $lineVal) { $item["line"] = $lineVal } + if ($sides.Count -gt 0) { $item["sides"] = $sides } + $outItem = [ordered]@{} + foreach ($key in @('type', 'id', 'zOrder', 'name', 'picture', 'detail', 'text', + 'style', 'line', 'sides', 'begin', 'end', 'pictureSize')) { + if ($item.Contains($key)) { $outItem[$key] = $item[$key] } + } + $drawingsOrdered += , $outItem +} +if ($picturesOut.Count -gt 0) { $result["pictures"] = $picturesOut } +if ($drawingsOrdered.Count -gt 0) { $result["drawings"] = [array]$drawingsOrdered } + $result["areas"] = [array]$dslAreas # Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится diff --git a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py index 8f018f7e..fb140715 100644 --- a/.claude/skills/mxl-decompile/scripts/mxl-decompile.py +++ b/.claude/skills/mxl-decompile/scripts/mxl-decompile.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-decompile v1.26 — Decompile 1C spreadsheet to JSON +# mxl-decompile v1.27 — Decompile 1C spreadsheet to JSON # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills import argparse @@ -79,7 +79,7 @@ def format_tag_kind(): 'autoIndent': 'int', 'autoMarkIncomplete': 'bool', 'autoWidthCalculation': 'bool', 'backColor': 'color', 'border': 'line', 'borderColor': 'color', 'bottomBorder': 'line', 'bySelectedColumns': 'bool', 'columnSizeChange': 'enum', - 'detailsUse': 'enum', 'drawingBorder': 'int', + 'detailsUse': 'enum', 'drawingBorder': 'line', 'drawingHaveBottomBorder': 'bool', 'drawingHaveLeftBorder': 'bool', 'drawingHaveRightBorder': 'bool', 'drawingHaveTopBorder': 'bool', 'editFormat': 'ml', 'fillType': 'enum', 'font': 'int', 'format': 'ml', @@ -431,6 +431,77 @@ def main(): return None return raw_formats[idx - 1] + # --- 4-bis. Палитра картинок и рисунки --- + # Картинка хранится ссылкой на библиотеку либо данными base64; ссылки на палитру (у рисунка + # pictureIndex, у ячейки picIndex) — 1-based, сам — 0-based. + pictures_out = OrderedDict() + picture_key = {} + for pic_i, pic_node in enumerate(findall(root, "d:picture"), start=1): + inner = find(pic_node, "d:picture") + if inner is None: + continue + name = f'pic{pic_i}' + entry = OrderedDict() + if inner.get("ref"): + entry["ref"] = inner.get("ref") + elif (inner.text or "").strip(): + entry["data"] = (inner.text or "").strip() + if inner.get("t") is not None: + entry["transparent"] = inner.get("t") == "true" + # Пиксель прозрачного цвета: координата внутри самой картинки (проверено — + # это не её размеры). В корпусе встречается без атрибута t. + if inner.get("tx") is not None: + entry["transparentPixel"] = OrderedDict([("x", int(inner.get("tx"))), + ("y", int(inner.get("ty") or 0))]) + pictures_out[name] = entry + picture_key[pic_i] = name + + drawing_names = {} + for ni_node in findall(root, "d:namedItem"): + if (ni_node.get(f'{{{XSI_NS}}}type') or '') == 'NamedItemDrawing': + drawing_names[text_of(find(ni_node, "d:drawingID")) or ''] = text_of(find(ni_node, "d:name")) or '' + + drawings_out = [] + for dr_i, dr in enumerate(findall(root, "d:drawing"), start=1): + item = OrderedDict() + item["type"] = text_of(find(dr, "d:drawingType")) or "Picture" + did = text_of(find(dr, "d:id")) or str(dr_i) + if did != str(dr_i): + item["id"] = int(did) + zorder = text_of(find(dr, "d:zOrder")) or str(dr_i) + if zorder != str(dr_i): + item["zOrder"] = int(zorder) + if did in drawing_names: + item["name"] = drawing_names[did] + pic_ref = text_of(find(dr, "d:pictureIndex")) + if pic_ref and int(pic_ref) in picture_key: + item["picture"] = picture_key[int(pic_ref)] + detail = text_of(find(dr, "d:detailParameter")) + if detail: + item["detail"] = detail + t_el = find(dr, "d:text") + if t_el is not None: + by_lang = OrderedDict() + for it in findall(t_el, "v8:item"): + by_lang[text_of(find(it, "v8:lang")) or ''] = text_of(find(it, "v8:content")) or '' + item["text"] = by_lang + # Оформление разбирается позже: линию нужно развернуть в описание, а помощник для + # этого появляется вместе с палитрой стилей. + fmt_idx = int_of(find(dr, "d:formatIndex")) + + def anchor(prefix, off_row, off_col): + return OrderedDict([("row", int_of(find(dr, f'd:{prefix}Row')) + 1), + ("col", int_of(find(dr, f'd:{prefix}Column')) + 1), + ("dy", int_of(find(dr, f'd:{off_row}'))), + ("dx", int_of(find(dr, f'd:{off_col}')))]) + item["begin"] = anchor("begin", "beginRowOffset", "beginColumnOffset") + item["end"] = anchor("end", "endRowOffset", "endColumnOffset") + psize = text_of(find(dr, "d:pictureSize")) + if psize and psize != "Stretch": + item["pictureSize"] = psize + item["_fmt"] = fmt_idx + drawings_out.append(item) + # --- 4a. Колонтитулы и параметры печати --- # Слот колонтитула устроен как ячейка: ссылка на формат плюс текст. Признак вывода и стартовую # страницу читаем только у записи, где есть : палитра дедуплицирована, и колонтитул без @@ -790,7 +861,11 @@ def main(): # общий на многие ячейки, а они индивидуальны. Компилятор таких ключей не знает, так что # в DSL они были чистым шумом. NON_STYLE_TAGS = ("width", "height", "fillType", "font", - "containsValue", "valueType", "controlType") + "containsValue", "valueType", "controlType", + # Линия рисунка и её стороны — свойства рисунка, а не общего оформления: + # на корпусе все 2 135 записей с ними принадлежат только рисункам. + "drawingBorder", "drawingHaveLeftBorder", "drawingHaveTopBorder", + "drawingHaveRightBorder", "drawingHaveBottomBorder") BORDER_TAGS = ("border", "leftBorder", "topBorder", "rightBorder", "bottomBorder") def line_to_dsl(idx): @@ -934,6 +1009,19 @@ def main(): style_keys[nkey] = nfmt format_to_style_key[cell["Note"]["FormatIdx"]] = nkey + # Формат рисунка — ещё один владелец записи палитры, наравне с ячейкой, строкой, колонкой + # и примечанием: без этого оформление рисунка теряло имя и вырезалось как неиспользуемое. + for _item in drawings_out: + _fi = _item.get("_fmt") or 0 + if not _fi: + continue + _fmt = get_format(_fi) + if _fmt and (style_props(_fmt) or _fmt["FontIdx"] >= 0): + _key = get_style_key(_fmt) + if _key not in style_keys: + style_keys[_key] = _fmt + format_to_style_key[_fi] = _key + def row_style_fmt(fmt): """Оформление строки без её собственных свойств: скрытие уезжает инлайном к height, поэтому в именованный стиль попадать не должно.""" @@ -1509,6 +1597,15 @@ def main(): for name in column_styles_of(cs).values(): if name: used_styles.add(name) + # Рисунок — пятый владелец формата. Имя ему присваивается позже, при сборке результата, + # поэтому здесь считаем его из ссылки на палитру: иначе стиль рисунка вырежется как + # неиспользуемый, а ссылка на него останется висячей. + for _item in drawings_out: + _fi = _item.get("_fmt") or 0 + if _fi: + _name = get_style_name(_fi) + if _name != "default": + used_styles.add(_name) to_remove = [s for s in style_defs if s not in used_styles] for s in to_remove: del style_defs[s] @@ -1552,6 +1649,43 @@ def main(): if print_settings: result["printSettings"] = print_settings + # Оформление рисунка: линия и её стороны — его собственные ключи, общее — именованный стиль. + # Порядок ключей задаём здесь же, чтобы он не зависел от того, что разбиралось раньше. + ordered = [] + for item in drawings_out: + fmt_idx = item.pop("_fmt", 0) + if "text" in item: + item["text"] = get_dsl_text(item["text"]) + line_val = None + sides = OrderedDict() + if fmt_idx: + fmt = get_format(fmt_idx) + props = fmt["Props"] if fmt else {} + if "drawingBorder" in props: + line_val = line_to_dsl(int(props["drawingBorder"])) + for side in ("Left", "Top", "Right", "Bottom"): + tag = f'drawingHave{side}Border' + if tag in props: + sides[side.lower()] = props[tag] == "true" + name = get_style_name(fmt_idx) + if name != "default": + item["style"] = name + if line_val is not None: + item["line"] = line_val + if sides: + item["sides"] = sides + out_item = OrderedDict() + for key in ("type", "id", "zOrder", "name", "picture", "detail", "text", + "style", "line", "sides", "begin", "end", "pictureSize"): + if key in item: + out_item[key] = item[key] + ordered.append(out_item) + drawings_out = ordered + if pictures_out: + result["pictures"] = pictures_out + if drawings_out: + result["drawings"] = drawings_out + result["areas"] = dsl_areas # Именованные области, не выразимые блоком, — координатами. Тип не пишем: он выводится diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 index e7e18579..1c82fd1d 100644 --- a/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 +++ b/.claude/skills/mxl-validate/scripts/mxl-validate.ps1 @@ -1,4 +1,4 @@ -# mxl-validate v1.6 — Validate 1C spreadsheet +# mxl-validate v1.7 — Validate 1C spreadsheet # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills param( [Alias('Path')] @@ -408,6 +408,16 @@ foreach ($drawing in $root.SelectNodes("d:drawing", $nsMgr)) { Report-Error "Drawing id=${drawId}: pictureIndex=$picIdx > picture count ($pictureCount)" } } + # Оформление рисунка — такая же запись палитры форматов, как у ячейки, и висячая + # ссылка на неё так же валит загрузку макета. + $fmtIdxNode = $drawing.SelectSingleNode("d:formatIndex", $nsMgr) + if ($fmtIdxNode) { + $fmtIdx = [int]$fmtIdxNode.InnerText + if ($fmtIdx -gt $formatCount) { + $drawId = $drawing.SelectSingleNode("d:id", $nsMgr).InnerText + Report-Error "Drawing id=${drawId}: formatIndex=$fmtIdx > format palette size ($formatCount)" + } + } } # --- Check 13: value cells (input fields) --- diff --git a/.claude/skills/mxl-validate/scripts/mxl-validate.py b/.claude/skills/mxl-validate/scripts/mxl-validate.py index ac0c3342..57a3f27e 100644 --- a/.claude/skills/mxl-validate/scripts/mxl-validate.py +++ b/.claude/skills/mxl-validate/scripts/mxl-validate.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -# mxl-validate v1.6 — Validate 1C spreadsheet document Template.xml +# mxl-validate v1.7 — Validate 1C spreadsheet document Template.xml # Source: https://github.com/Nikolay-Shirokov/cc-1c-skills """Validates spreadsheet Template.xml: height, palette refs, column/row indices, areas, merges.""" import sys, os, argparse @@ -405,6 +405,16 @@ def main(): draw_id = draw_id_node.text if draw_id_node is not None else '?' r.error(f'Drawing id={draw_id}: pictureIndex={pic_idx} > picture count ({picture_count})') + # Оформление рисунка — такая же запись палитры форматов, как у ячейки, и висячая + # ссылка на неё так же валит загрузку макета. + fmt_idx_node = drawing.find(f'{{{NS_D}}}formatIndex') + if fmt_idx_node is not None and fmt_idx_node.text: + fmt_idx = int(fmt_idx_node.text) + if fmt_idx > format_count: + draw_id_node = drawing.find(f'{{{NS_D}}}id') + draw_id = draw_id_node.text if draw_id_node is not None else '?' + r.error(f'Drawing id={draw_id}: formatIndex={fmt_idx} > format palette size ({format_count})') + # --- Check 13: value cells (input fields) --- # Свойства containsValue/valueType/controlType принадлежат ЯЧЕЙКЕ: на корпусе ERP # (370 197 ссылок) на такие записи палитры ссылаются только , ни строка, ни колонка, diff --git a/docs/1c-spreadsheet-spec.md b/docs/1c-spreadsheet-spec.md index 9427ffd1..ef59ba2c 100644 --- a/docs/1c-spreadsheet-spec.md +++ b/docs/1c-spreadsheet-spec.md @@ -708,15 +708,85 @@ current-config не объявляет, поэтому вынести объяв подсказки (`verticalAlignment: Top` + `style:ToolTipTextColor` + `style:ToolTipBackColor`), 105 — он же с заливкой `#FFFAD9`. -## Ресурсы картинок +## Палитра картинок + +Идёт последним блоком документа. Внутренний `` 0-based, а ссылки на него — 1-based: +`` у рисунка, `picIndex` в записи формата у картинки в ячейке. ```xml 0 - + + + + 1 + iVBORw0KGgoAAAANSUhEUgAAAG4AAAA2CAIAAACp5Ds2AAAAAXNSR0IArs4c6QAA +AARnQUlBAACxjwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAA ``` +Данные base64 платформа переносит по строкам тем же переводом, что и весь файл (CRLF), — +в отличие от блоба настроек элемента управления, где перенос голый LF. + +Атрибуты внутреннего `` по корпусу ERP (10 130 записей): + +| Вид | Сколько | Смысл | +|-----|--------:|-------| +| данные без атрибутов | 9 896 | картинка хранится в макете | +| `` | 151 | картинка не задана; ровно по одной такой записи на макет | +| `ref="v8ui:Имя"` | 83 | ссылка на библиотеку картинок платформы | +| `tx`, `ty` при данных | 9 | координата пикселя внутри картинки (проверено: это НЕ её размеры — у `tx=97 ty=83` сама картинка 368×117) | + +Атрибут `t` (прозрачность) в корпусе не встречается, но платформа его пишет: `t="false"` +появляется у картинки, вставленной в ячейку. + +## Рисунки + +Рисунок — объект поверх сетки: картинка, фигура, надпись, диаграмма. Лежит в `` +перед палитрами; геометрия задана двумя якорями «ячейка + смещение в точках». + +```xml + + Picture + 3 + 3 + 1 + 1 + 0 + 0 + 3 + 2 + 34 + 115 + 4 + Proportionally + 2 + Расшифровка + +``` + +Координаты строк и колонок здесь 0-based, в отличие от `` ячейки. `` и `` +самостоятельны: на корпусе они расходятся у 8 253 рисунков из 11 268, так что выводить их +из порядка следования нельзя. `Stretch` — умолчание, пишется всегда. + +Имя рисунка хранится отдельно, среди именованных элементов: + +```xml + + Логотип + 3 + +``` + +Оформление рисунка — обычная запись палитры форматов, но с собственными тегами: `drawingBorder` +(ссылка в палитру линий, вид `SpreadsheetDocumentDrawingLineType`) и +`drawingHaveLeftBorder` / `drawingHaveTopBorder` / `drawingHaveRightBorder` / +`drawingHaveBottomBorder`. У ячейки таких свойств не бывает: все 2 135 записей корпуса с ними +принадлежат рисункам. Остальное в записи — общие свойства (заливка, шрифт, выравнивание). + +Тело диаграмм (`Chart`, `GanttChart`) лежит в том же `` в отдельном пространстве имён +и здесь не разбирается. + ## Типичная структура макета печатной формы Печатная форма обычно состоит из именованных горизонтальных областей: diff --git a/docs/mxl-dsl-format-properties.md b/docs/mxl-dsl-format-properties.md index ddd7a37e..7af4d710 100644 --- a/docs/mxl-dsl-format-properties.md +++ b/docs/mxl-dsl-format-properties.md @@ -64,8 +64,6 @@ | `picHorizontalAlignment` | перечисление | `Auto`, `Center`, `Left`, `Right` | | `picVerticalAlignment` | перечисление | `Top`, `Center`, `Bottom` | | `textPosition` | перечисление | Положение текста относительно картинки: `Auto`, `Top`, `Right`, `Bottom` | -| `drawingBorder` | число | Рамка рисунка | -| `drawingHaveLeftBorder`, `drawingHaveTopBorder`, `drawingHaveRightBorder`, `drawingHaveBottomBorder` | да/нет | Наличие стороны рамки рисунка | ## Чего в стиле нет @@ -74,4 +72,6 @@ - `fillType` — выводится из того, каким ключом задано содержимое ячейки (`text` / `param` / `template`); - `containsValue`, `valueType`, `controlType` — свойства конкретной ячейки, а не общего - оформления: задаются ключами ячейки `valueType` и `controlType`. + оформления: задаются ключами ячейки `valueType` и `controlType`; +- линия рисунка и её стороны — свойства рисунка: задаются его ключами `line` и `sides` + (у ячейки такого свойства нет вовсе). diff --git a/docs/mxl-dsl-spec.md b/docs/mxl-dsl-spec.md index 0ee9bc44..f309e88b 100644 --- a/docs/mxl-dsl-spec.md +++ b/docs/mxl-dsl-spec.md @@ -91,6 +91,8 @@ | `columnGroups` | нет | — | Группы колонок (см. ниже) | | `header` / `footer` | нет | — | Верхний и нижний колонтитулы (см. ниже) | | `printSettings` | нет | — | Параметры печати (см. ниже) | +| `pictures` | нет | — | Палитра картинок: ссылки на библиотеку платформы или данные (см. ниже) | +| `drawings` | нет | — | Рисунки: картинки, фигуры, надписи (см. ниже) | ## Области (`areas[]`) @@ -384,6 +386,54 @@ Порядок в списке не важен: компилятор пишет группы так же, как платформа, — родитель раньше вложенных, по возрастанию начала. +## Рисунки и картинки + +Рисунок — объект поверх сетки ячеек: картинка, фигура или надпись. Его положение задают два +якоря: ячейка плюс смещение в точках от её левого верхнего угла. + +```json +"pictures": { + "знак": { "ref": "v8ui:Стоп48" }, + "логотип": { "data": "iVBORw0KGgo...", "transparent": false } +}, +"drawings": [ + { "type": "Picture", "picture": "логотип", "name": "Логотип", + "begin": { "row": 1, "col": 1 }, + "end": { "row": 3, "col": 2, "dy": 34, "dx": 115 }, + "pictureSize": "Proportionally", "detail": "ПараметрРасшифровки" }, + { "type": "Rectangle", "style": "заливка", + "line": { "style": "Dashed", "width": 2 }, + "sides": { "left": true, "top": true, "right": false, "bottom": false }, + "begin": { "row": 5, "col": 1 }, "end": { "row": 6, "col": 3 } }, + { "type": "Text", "text": { "ru": "Подпись", "en": "Signature" }, + "begin": { "row": 8, "col": 1 }, "end": { "row": 8, "col": 3, "dy": 12 } } +] +``` + +| Поле | Обяз. | По умолч. | Описание | +|------|:-----:|-----------|----------| +| `type` | нет | `Picture` | `Picture`, `Rectangle`, `Ellipse`, `Line`, `Text`, `Chart`, `GanttChart` | +| `begin` / `end` | да | — | Якоря: `row`, `col` (1-based) и смещения `dy`, `dx` в точках | +| `picture` | нет | — | Имя записи из `pictures` | +| `pictureSize` | нет | `Stretch` | Как картинка заполняет прямоугольник: `Stretch`, `AutoSize`, `Proportionally`, `RealSize` | +| `text` | нет | — | Надпись: строка или объект «язык → текст» | +| `name` | нет | — | Имя рисунка, по которому к нему обращаются из кода | +| `detail` | нет | — | Параметр расшифровки | +| `style` | нет | — | Именованный стиль: заливка, шрифт, выравнивание надписи | +| `line` | нет | — | Линия рисунка (она же его рамка) — как `border` у ячейки | +| `sides` | нет | — | Какие стороны рамки видны: `left`, `top`, `right`, `bottom` | +| `id` | нет | номер по порядку | Идентификатор рисунка | +| `zOrder` | нет | номер по порядку | Порядок перекрытия: чем больше, тем выше | + +Порядок в списке `drawings` — порядок в документе; кто кого перекрывает, задаёт `zOrder`. + +Запись в `pictures` — либо ссылка на библиотеку картинок платформы (`ref`), либо сами данные +в base64 (`data`). Пустая запись `{}` — картинка не задана, такое в макетах встречается. +С данными сочетаются `transparent` (прозрачность включена) и `transparentPixel` +(`{ "x": …, "y": … }` — координата пикселя, чей цвет считается прозрачным). + +Одну запись `pictures` могут использовать несколько рисунков — данные в макете не дублируются. + ## `rowStyle` — оформление строки Стиль применяется ко ВСЕЙ ширине строки: позиции без явных ячеек получают тот же стиль. Так в табличных строках получаются сплошные рамки. Он же становится оформлением самой строки — именно так платформа хранит строку, оформленную целиком. @@ -399,7 +449,7 @@ round-trip** (`/mxl-decompile` → `/mxl-compile`): в JSON оно не попа XML не возвращается. - объединения, не привязанные к ячейке (по всей высоте или ширине документа); -- рисунки и картинки, в том числе штрихкоды; +- настройки диаграмм (`Chart`, `GanttChart`) — сам рисунок сохраняется, его содержимое нет; - область печати. Пересборка макета из DSL — это полная перегенерация, а не точечная правка XML, поэтому diff --git a/tests/skills/cases/mxl-compile/drawings.json b/tests/skills/cases/mxl-compile/drawings.json new file mode 100644 index 00000000..0f79e7aa --- /dev/null +++ b/tests/skills/cases/mxl-compile/drawings.json @@ -0,0 +1,34 @@ +{ + "name": "Рисунки: типы, картинки, линия, стороны, расшифровка", + "input": { + "columns": 4, + "styles": { + "заливка": { "backColor": "web:LightYellow" } + }, + "pictures": { + "стоп": { "ref": "v8ui:Стоп48" }, + "точка": { "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==", "transparent": false } + }, + "areas": [{ "rows": [["Бланк"]] }], + "drawings": [ + { "type": "Picture", "picture": "стоп", "name": "Знак", + "begin": { "row": 1, "col": 1, "dy": 0, "dx": 0 }, + "end": { "row": 2, "col": 2, "dy": 20, "dx": 40 }, + "pictureSize": "Proportionally" }, + { "type": "Picture", "picture": "точка", "detail": "ПараметрРасшифровки", + "begin": { "row": 3, "col": 1 }, + "end": { "row": 3, "col": 2, "dy": 34, "dx": 115 } }, + { "type": "Rectangle", "style": "заливка", + "line": { "style": "Dashed", "width": 2 }, + "sides": { "left": true, "top": true, "right": false, "bottom": false }, + "begin": { "row": 5, "col": 1 }, "end": { "row": 6, "col": 3 } }, + { "type": "Ellipse", "line": "Solid", + "begin": { "row": 8, "col": 1 }, "end": { "row": 9, "col": 2 } }, + { "type": "Line", "zOrder": 9, + "begin": { "row": 11, "col": 1 }, "end": { "row": 11, "col": 4 } }, + { "type": "Text", "id": 42, "text": { "ru": "Подпись", "en": "Signature" }, + "begin": { "row": 13, "col": 1 }, "end": { "row": 13, "col": 3, "dy": 12 } } + ] + }, + "params": { "outputPath": "Template.xml" } +} diff --git a/tests/skills/cases/mxl-compile/error-picture-transparent-without-data.json b/tests/skills/cases/mxl-compile/error-picture-transparent-without-data.json new file mode 100644 index 00000000..a87ed7f0 --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-picture-transparent-without-data.json @@ -0,0 +1,10 @@ +{ + "name": "Ошибка: прозрачность задана у картинки без данных", + "input": { + "columns": 2, + "pictures": { "пусто": { "transparent": true } }, + "areas": [{ "rows": [["Бланк"]] }] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "pictures[пусто]: 'transparent' and 'transparentPixel' require 'data'" +} diff --git a/tests/skills/cases/mxl-compile/error-picture-unknown.json b/tests/skills/cases/mxl-compile/error-picture-unknown.json new file mode 100644 index 00000000..b3f11f2b --- /dev/null +++ b/tests/skills/cases/mxl-compile/error-picture-unknown.json @@ -0,0 +1,13 @@ +{ + "name": "Ошибка: рисунок ссылается на необъявленную картинку", + "input": { + "columns": 2, + "areas": [{ "rows": [["Бланк"]] }], + "drawings": [ + { "type": "Picture", "picture": "нет-такой", + "begin": { "row": 1, "col": 1 }, "end": { "row": 2, "col": 2 } } + ] + }, + "params": { "outputPath": "Template.xml" }, + "expectError": "drawings[1]: unknown 'picture' \"нет-такой\" — not declared in pictures" +} diff --git a/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Рисунки2.xml b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Рисунки2.xml new file mode 100644 index 00000000..e65de409 --- /dev/null +++ b/tests/skills/cases/mxl-compile/fixtures/platform-roundtrip/Рисунки2.xml @@ -0,0 +1,284 @@ + + + + ru + ru + + ru + Русский + Русский + + + en + Английский + Английский + + + + 5 + + + 0 + + true + + + + Picture + 1 + 0 + ПараметрРасшифровки + 1 + 0 + 3 + 34 + 1 + 0 + 1 + 115 + false + Stretch + 1 + 1 + + + Picture + 2 + 1 + 6 + 0 + 8 + 34 + 1 + 0 + 1 + 115 + false + RealSize + 2 + 1 + + + Picture + 3 + 2 + 12 + 0 + 14 + 34 + 1 + 0 + 1 + 115 + false + Proportionally + 3 + 1 + + + Picture + 4 + 3 + 18 + 0 + 20 + 34 + 1 + 0 + 1 + 115 + false + Tile + 4 + 1 + + + Picture + 5 + 0 + 23 + 0 + 25 + 34 + 1 + 0 + 1 + 115 + false + AutoSize + 5 + 1 + + + Picture + 6 + 0 + 28 + 0 + 30 + 34 + 1 + 0 + 1 + 115 + false + ByFontSize + 6 + 1 + + + Picture + 7 + 1 + 1 + 0 + 2 + 31 + 3 + 0 + 3 + 60 + false + Stretch + 7 + 2 + + + Line + 8 + 4 + 7 + 19 + 4 + 24 + 2 + 106 + 4 + 103 + false + Stretch + 8 + + + Rectangle + 9 + 5 + 8 + 36 + 12 + 10 + 2 + 108 + 4 + 125 + false + Stretch + 9 + + + Ellipse + 10 + 6 + 14 + 22 + 17 + 12 + 2 + 134 + 4 + 139 + false + Stretch + 10 + + + Text + 11 + 7 + + + ru + Текст + + + 19 + 38 + 23 + 17 + 2 + 101 + 4 + 146 + false + Stretch + 11 + + true + 8 + 31 + 31 + + Звездочка + 7 + + + None + + + Dashed + + + DashDottedDotted + + + DashDotted + + + 0 + + + d3p1:HoneyDew + + + style:SpecialTextColor + Pattern4 + + + Top + style:FieldBackColor + + + 1 + Top + style:FieldBackColor + + + 2 + Top + style:FieldBackColor + + + 3 + Top + style:FieldBackColor + + + 72 + + + 0 + + + + 1 + iVBORw0KGgoAAAANSUhEUgAAABkAAAAeCAYAAADZ7LXbAAAAAXNSR0IArs4c6QAA +AARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAE9SURBVEhL7ZOh +coQwEIb7tBExmBjEGUwNBhGDiUH0DWIQmJgYREwM5gwCcwZT83cLuWlhrhOCqOJT +uwszX3Y3ecM/cEmSuCRJXJIkTkmszJFLG7I4JyQ9JGNgTFJ0jFOddCVJyi5kcaKS +XmZgPIdyc6gAj7ZA0T5CBsyuhqDustqFypaoZPjIwZfxCNRP0aChhzWcrUS2fOe4 +6XEt7jg8Lt9rNGWJxk/wWkFpj8k3KMsGuvfhr9ek74RGs3bGqbNQi5Akme8W1hnI +jIGLGp2zcPefXf1FVPI5GqhCLKfPnm/jMWAIezcVXQzqShQKZlpre6ISffseDQmq +X4/PVKhMiAm7iOi/9zZUtsTHNXkYt701rubgu4WMNEa6Ey9JXzzRCDq1aEIWw56R +eChaPMsURcc41UkqlySJS5IA8AW5c2KJOTY6eAAAAABJRU5ErkJggg== + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/pictures-empty-and-transparent.json b/tests/skills/cases/mxl-compile/pictures-empty-and-transparent.json new file mode 100644 index 00000000..a58b4d42 --- /dev/null +++ b/tests/skills/cases/mxl-compile/pictures-empty-and-transparent.json @@ -0,0 +1,20 @@ +{ + "name": "Картинки: пустая запись и пиксель прозрачного цвета", + "input": { + "columns": 2, + "pictures": { + "пусто": {}, + "точка": { + "data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==", + "transparent": true, + "transparentPixel": { "x": 3, "y": 5 } + } + }, + "areas": [{ "rows": [["Бланк"]] }], + "drawings": [ + { "type": "Picture", "picture": "точка", + "begin": { "row": 1, "col": 1 }, "end": { "row": 2, "col": 2 } } + ] + }, + "params": { "outputPath": "Template.xml" } +} diff --git a/tests/skills/cases/mxl-compile/platform-drawings.json b/tests/skills/cases/mxl-compile/platform-drawings.json new file mode 100644 index 00000000..8066e532 --- /dev/null +++ b/tests/skills/cases/mxl-compile/platform-drawings.json @@ -0,0 +1,13 @@ +{ + "name": "Раундтрип макета ПЛАТФОРМЫ байт в байт: Рисунки2", + "setup": "fixture:platform-roundtrip", + "preRun": [ + { "script": "mxl-decompile/scripts/mxl-decompile", + "args": { "-TemplatePath": "Рисунки2.xml", "-OutputPath": "back.json" }, + "cwd": "{workDir}" } + ], + "params": { "outputPath": "out.xml" }, + "noSnapshot": "эталон — файл платформы, сверяется expect.filesEqual", + "expect": { "filesEqual": { "actual": "out.xml", "expected": "Рисунки2.xml" } }, + "inputFrom": "back.json" +} diff --git a/tests/skills/cases/mxl-compile/snapshots/drawings/Template.xml b/tests/skills/cases/mxl-compile/snapshots/drawings/Template.xml new file mode 100644 index 00000000..0c1d2c44 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/drawings/Template.xml @@ -0,0 +1,176 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 4 + + + 0 + + + + 0 + + + ru + Бланк + + + + + + + + Picture + 1 + 0 + 0 + 0 + 1 + 20 + 0 + 0 + 1 + 40 + false + Proportionally + 1 + 1 + + + Picture + 2 + 0 + ПараметрРасшифровки + 2 + 0 + 2 + 34 + 0 + 0 + 1 + 115 + false + Stretch + 2 + 2 + + + Rectangle + 3 + 1 + 4 + 0 + 5 + 0 + 0 + 0 + 2 + 0 + false + Stretch + 3 + + + Ellipse + 4 + 2 + 7 + 0 + 8 + 0 + 0 + 0 + 1 + 0 + false + Stretch + 4 + + + Line + 5 + 0 + 10 + 0 + 10 + 0 + 0 + 0 + 3 + 0 + false + Stretch + 9 + + + Text + 42 + 0 + + + ru + Подпись + + + en + Signature + + + 12 + 0 + 12 + 12 + 0 + 0 + 2 + 0 + false + Stretch + 6 + + true + 3 + 13 + 13 + + Знак + 1 + + + Dashed + + + Solid + + + 0 + true + true + false + false + d3p1:LightYellow + + + 1 + + + 10 + + + 0 + + + + 1 + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg== + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-compile/snapshots/pictures-empty-and-transparent/Template.xml b/tests/skills/cases/mxl-compile/snapshots/pictures-empty-and-transparent/Template.xml new file mode 100644 index 00000000..472b1267 --- /dev/null +++ b/tests/skills/cases/mxl-compile/snapshots/pictures-empty-and-transparent/Template.xml @@ -0,0 +1,63 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 2 + + + 0 + + + + 0 + + + ru + Бланк + + + + + + + + Picture + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + false + Stretch + 1 + 2 + + true + 1 + 2 + 2 + + 10 + + + 0 + + + + 1 + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGP4z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg== + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/roundtrip-drawings.json b/tests/skills/cases/mxl-decompile/roundtrip-drawings.json new file mode 100644 index 00000000..4addbb43 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/roundtrip-drawings.json @@ -0,0 +1,30 @@ +{ + "name": "Roundtrip — рисунки и палитра картинок", + "preRun": [ + { "script": "mxl-compile/scripts/mxl-compile", + "input": { + "columns": 4, + "styles": { "заливка": { "backColor": "web:LightYellow" } }, + "pictures": { + "стоп": { "ref": "v8ui:Стоп48" }, + "пусто": {} + }, + "areas": [{ "rows": [["Бланк"]] }], + "drawings": [ + { "type": "Picture", "picture": "стоп", "name": "Знак", + "begin": { "row": 1, "col": 1 }, "end": { "row": 2, "col": 2, "dy": 20, "dx": 40 }, + "pictureSize": "Proportionally" }, + { "type": "Rectangle", "style": "заливка", "line": { "style": "Dashed", "width": 2 }, + "sides": { "left": true, "top": true, "right": false, "bottom": false }, + "begin": { "row": 4, "col": 1 }, "end": { "row": 5, "col": 3 } }, + { "type": "Text", "id": 42, "text": { "ru": "Подпись", "en": "Signature" }, + "begin": { "row": 7, "col": 1 }, "end": { "row": 7, "col": 3, "dy": 12 } } + ] + }, + "args": { "-JsonPath": "{inputFile}", "-OutputPath": "Template.xml" }, + "cwd": "{workDir}" } + ], + "params": { "templatePath": "Template.xml" }, + "args_extra": ["-OutputPath", "{workDir}/back.json"], + "expect": { "files": ["back.json"], "stdoutContains": "[OK] Decompiled:" } +} diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-drawings/Template.xml b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-drawings/Template.xml new file mode 100644 index 00000000..5632d156 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-drawings/Template.xml @@ -0,0 +1,120 @@ + + + + ru + ru + + ru + Русский + Русский + + + + 4 + + + 0 + + + + 0 + + + ru + Бланк + + + + + + + + Picture + 1 + 0 + 0 + 0 + 1 + 20 + 0 + 0 + 1 + 40 + false + Proportionally + 1 + 1 + + + Rectangle + 2 + 1 + 3 + 0 + 4 + 0 + 0 + 0 + 2 + 0 + false + Stretch + 2 + + + Text + 42 + 0 + + + ru + Подпись + + + en + Signature + + + 6 + 0 + 6 + 12 + 0 + 0 + 2 + 0 + false + Stretch + 3 + + true + 2 + 7 + 7 + + Знак + 1 + + + Dashed + + + 0 + true + true + false + false + d3p1:LightYellow + + + 10 + + + 0 + + + + 1 + + + \ No newline at end of file diff --git a/tests/skills/cases/mxl-decompile/snapshots/roundtrip-drawings/back.json b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-drawings/back.json new file mode 100644 index 00000000..47066e83 --- /dev/null +++ b/tests/skills/cases/mxl-decompile/snapshots/roundtrip-drawings/back.json @@ -0,0 +1,13 @@ +{ + "columns": 4, + "defaultWidth": 10, + "fonts": {}, + "styles": { "bg": { "backColor": "web:LightYellow" } }, + "pictures": { "pic1": { "ref": "v8ui:Стоп48" }, "pic2": {} }, + "drawings": [ + { "type": "Picture", "name": "Знак", "picture": "pic1", "begin": { "row": 1, "col": 1, "dy": 0, "dx": 0 }, "end": { "row": 2, "col": 2, "dy": 20, "dx": 40 }, "pictureSize": "Proportionally" }, + { "type": "Rectangle", "style": "bg", "line": { "style": "Dashed", "width": 2 }, "sides": { "left": true, "top": true, "right": false, "bottom": false }, "begin": { "row": 4, "col": 1, "dy": 0, "dx": 0 }, "end": { "row": 5, "col": 3, "dy": 0, "dx": 0 } }, + { "type": "Text", "id": 42, "text": { "ru": "Подпись", "en": "Signature" }, "begin": { "row": 7, "col": 1, "dy": 0, "dx": 0 }, "end": { "row": 7, "col": 3, "dy": 12, "dx": 0 } } + ], + "areas": [{ "rows": [["Бланк"]] }] +} \ No newline at end of file