Отчёт
This commit is contained in:
143
Report/append_sources_to_report.py
Normal file
143
Report/append_sources_to_report.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Append source code listings to a Markdown report.
|
||||||
|
|
||||||
|
Usage example:
|
||||||
|
python3 Report/append_sources_to_report.py \
|
||||||
|
--input Report/lab2/zivro-lab2-report.md \
|
||||||
|
--output Report/lab2/zivro-lab2-report-with-code.md \
|
||||||
|
--base . \
|
||||||
|
--include "Minint/**/*.cs" \
|
||||||
|
--include "Minint.Core/**/*.cs" \
|
||||||
|
--include "Minint.Infrastructure/**/*.cs" \
|
||||||
|
--include "Minint.Tests/**/*.cs"
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
EXT_TO_LANG: dict[str, str] = {
|
||||||
|
".cs": "csharp",
|
||||||
|
".axaml": "xml",
|
||||||
|
".xml": "xml",
|
||||||
|
".json": "json",
|
||||||
|
".md": "markdown",
|
||||||
|
".sh": "bash",
|
||||||
|
".py": "python",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(description="Append code listings to report markdown.")
|
||||||
|
parser.add_argument("--input", required=True, help="Path to source markdown report.")
|
||||||
|
parser.add_argument("--output", required=True, help="Path to resulting markdown report.")
|
||||||
|
parser.add_argument(
|
||||||
|
"--base",
|
||||||
|
default=".",
|
||||||
|
help="Project root path used for resolving include patterns.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--include",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
help="Glob pattern relative to --base. Can be passed multiple times.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--exclude-substring",
|
||||||
|
action="append",
|
||||||
|
default=["/bin/", "/obj/", ".git/"],
|
||||||
|
help="Skip files that contain this substring in path.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--title",
|
||||||
|
default="Приложение A. Исходные тексты",
|
||||||
|
help="Heading title for generated appendix.",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def detect_lang(path: Path) -> str:
|
||||||
|
return EXT_TO_LANG.get(path.suffix.lower(), "text")
|
||||||
|
|
||||||
|
|
||||||
|
def should_skip(path: Path, excludes: list[str]) -> bool:
|
||||||
|
normalized = str(path).replace("\\", "/")
|
||||||
|
return any(sub in normalized for sub in excludes)
|
||||||
|
|
||||||
|
|
||||||
|
def collect_files(base: Path, patterns: list[str], excludes: list[str]) -> list[Path]:
|
||||||
|
files: list[Path] = []
|
||||||
|
seen: set[Path] = set()
|
||||||
|
for pattern in patterns:
|
||||||
|
for p in base.glob(pattern):
|
||||||
|
if not p.is_file():
|
||||||
|
continue
|
||||||
|
if should_skip(p, excludes):
|
||||||
|
continue
|
||||||
|
rp = p.resolve()
|
||||||
|
if rp in seen:
|
||||||
|
continue
|
||||||
|
seen.add(rp)
|
||||||
|
files.append(rp)
|
||||||
|
files.sort(key=lambda p: str(p).replace("\\", "/"))
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def build_appendix(base: Path, files: list[Path], title: str) -> str:
|
||||||
|
lines: list[str] = []
|
||||||
|
lines.append("")
|
||||||
|
lines.append("---")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"## {title}")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(
|
||||||
|
f"Сформировано автоматически скриптом `Report/append_sources_to_report.py` (файлов: {len(files)})."
|
||||||
|
)
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
for i, file_path in enumerate(files, start=1):
|
||||||
|
rel = file_path.relative_to(base).as_posix()
|
||||||
|
lang = detect_lang(file_path)
|
||||||
|
content = file_path.read_text(encoding="utf-8", errors="replace").rstrip()
|
||||||
|
lines.append(f"### A.{i}. `{rel}`")
|
||||||
|
lines.append("")
|
||||||
|
lines.append(f"```{lang}")
|
||||||
|
if content:
|
||||||
|
lines.append(content)
|
||||||
|
lines.append("```")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
input_md = Path(args.input).resolve()
|
||||||
|
output_md = Path(args.output).resolve()
|
||||||
|
base = Path(args.base).resolve()
|
||||||
|
patterns = args.include or ["**/*.cs"]
|
||||||
|
|
||||||
|
if not input_md.is_file():
|
||||||
|
raise FileNotFoundError(f"Input markdown not found: {input_md}")
|
||||||
|
if not base.exists():
|
||||||
|
raise FileNotFoundError(f"Base path not found: {base}")
|
||||||
|
|
||||||
|
report_body = input_md.read_text(encoding="utf-8")
|
||||||
|
files = collect_files(base, patterns, args.exclude_substring)
|
||||||
|
appendix = build_appendix(base, files, args.title)
|
||||||
|
|
||||||
|
output_md.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output_md.write_text(report_body.rstrip() + "\n" + appendix + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
print(f"Input: {input_md}")
|
||||||
|
print(f"Output: {output_md}")
|
||||||
|
print(f"Files appended: {len(files)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
16
Report/header.tex
Normal file
16
Report/header.tex
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
\usepackage{fvextra}
|
||||||
|
\DefineVerbatimEnvironment{Highlighting}{Verbatim}{
|
||||||
|
breaklines,
|
||||||
|
breakanywhere,
|
||||||
|
commandchars=\\\{\},
|
||||||
|
fontsize=\small
|
||||||
|
}
|
||||||
|
|
||||||
|
\usepackage{xurl}
|
||||||
|
\urlstyle{same}
|
||||||
|
\setlength{\emergencystretch}{3em}
|
||||||
|
|
||||||
|
\usepackage{graphicx}
|
||||||
|
\setkeys{Gin}{width=\linewidth,height=0.80\textheight,keepaspectratio}
|
||||||
|
\usepackage{float}
|
||||||
|
\floatplacement{figure}{H}
|
||||||
BIN
Report/lab2/Screenshot.png
Normal file
BIN
Report/lab2/Screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 400 KiB |
38
Report/lab2/generate-pdf.sh
Executable file
38
Report/lab2/generate-pdf.sh
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPORT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
ROOT_DIR="$(cd "${REPORT_DIR}/.." && pwd)"
|
||||||
|
|
||||||
|
INPUT_MD="${SCRIPT_DIR}/zivro-lab2-report.md"
|
||||||
|
WITH_CODE_MD="${SCRIPT_DIR}/zivro-lab2-report-with-code.md"
|
||||||
|
OUTPUT_PDF="${SCRIPT_DIR}/zivro-lab2-report.pdf"
|
||||||
|
|
||||||
|
echo "[1/3] Генерация PNG из UML..."
|
||||||
|
python3 "${REPORT_DIR}/render_uml_png.py" \
|
||||||
|
--input-dir "${SCRIPT_DIR}/uml" \
|
||||||
|
--glob "*.puml"
|
||||||
|
|
||||||
|
echo "[2/3] Обновление версии отчёта с листингом кода..."
|
||||||
|
python3 "${REPORT_DIR}/append_sources_to_report.py" \
|
||||||
|
--input "${INPUT_MD}" \
|
||||||
|
--output "${WITH_CODE_MD}" \
|
||||||
|
--base "${ROOT_DIR}" \
|
||||||
|
--include "Minint/**/*.cs" \
|
||||||
|
--include "Minint.Core/**/*.cs" \
|
||||||
|
--include "Minint.Infrastructure/**/*.cs" \
|
||||||
|
--include "Minint.Tests/**/*.cs"
|
||||||
|
|
||||||
|
echo "[3/3] Генерация PDF..."
|
||||||
|
pandoc "${WITH_CODE_MD}" \
|
||||||
|
-f markdown+implicit_figures+link_attributes+tex_math_dollars \
|
||||||
|
-o "${OUTPUT_PDF}" \
|
||||||
|
--pdf-engine=xelatex \
|
||||||
|
--toc \
|
||||||
|
-V geometry:left=1.5cm,right=1.5cm,top=1.5cm,bottom=1.5cm \
|
||||||
|
-V mainfont="DejaVu Serif" \
|
||||||
|
-V monofont="DejaVu Sans Mono" \
|
||||||
|
-H "${REPORT_DIR}/header.tex"
|
||||||
|
|
||||||
|
echo "Готово: ${OUTPUT_PDF}"
|
||||||
BIN
Report/lab2/uml/lr2-container-serialization.png
Normal file
BIN
Report/lab2/uml/lr2-container-serialization.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
42
Report/lab2/uml/lr2-container-serialization.puml
Normal file
42
Report/lab2/uml/lr2-container-serialization.puml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
@startuml
|
||||||
|
title ЛР2 ИВ1: структура контейнера и сериализация
|
||||||
|
|
||||||
|
rectangle "Файл .minint" {
|
||||||
|
rectangle "Header" as H
|
||||||
|
rectangle "Documents[]" as D
|
||||||
|
}
|
||||||
|
|
||||||
|
rectangle "Header" {
|
||||||
|
card "Signature: MININT"
|
||||||
|
card "Version"
|
||||||
|
card "Width, Height"
|
||||||
|
card "DocumentCount"
|
||||||
|
card "Reserved[8]"
|
||||||
|
}
|
||||||
|
|
||||||
|
rectangle "Document" {
|
||||||
|
card "Name"
|
||||||
|
card "FrameDelayMs"
|
||||||
|
card "PaletteCount"
|
||||||
|
card "Palette RGBA[]"
|
||||||
|
card "LayerCount"
|
||||||
|
card "Layers[]"
|
||||||
|
}
|
||||||
|
|
||||||
|
rectangle "Layer" {
|
||||||
|
card "Name"
|
||||||
|
card "IsVisible"
|
||||||
|
card "Opacity"
|
||||||
|
card "PixelIndices[]"
|
||||||
|
}
|
||||||
|
|
||||||
|
H --> D
|
||||||
|
D --> "Document"
|
||||||
|
"Document" --> "Layer"
|
||||||
|
|
||||||
|
note bottom
|
||||||
|
PixelIndices хранят индексы палитры.
|
||||||
|
Ширина индекса в файле: 1..4 байта
|
||||||
|
в зависимости от размера палитры.
|
||||||
|
end note
|
||||||
|
@enduml
|
||||||
BIN
Report/lab2/uml/lr2-editor-workflow.png
Normal file
BIN
Report/lab2/uml/lr2-editor-workflow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
28
Report/lab2/uml/lr2-editor-workflow.puml
Normal file
28
Report/lab2/uml/lr2-editor-workflow.puml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
@startuml
|
||||||
|
title ЛР2 ИВ1: основной рабочий цикл растрового редактора
|
||||||
|
|
||||||
|
start
|
||||||
|
:Запуск приложения;
|
||||||
|
:Инициализация EditorViewModel и PixelCanvas;
|
||||||
|
|
||||||
|
repeat
|
||||||
|
:Ожидание действий пользователя;
|
||||||
|
if (Выбран файл контейнера?) then (да)
|
||||||
|
:Чтение .minint через MinintSerializer;
|
||||||
|
:Построение модели документа;
|
||||||
|
endif
|
||||||
|
|
||||||
|
if (Режим редактирования?) then (да)
|
||||||
|
:Инструмент Brush/Eraser/Fill/Select;
|
||||||
|
:Изменение пикселей активного слоя;
|
||||||
|
:Обновление холста (composite);
|
||||||
|
endif
|
||||||
|
|
||||||
|
if (Сохранить?) then (да)
|
||||||
|
:Запись контейнера в .minint;
|
||||||
|
endif
|
||||||
|
repeat while (Продолжать работу?) is (да)
|
||||||
|
|
||||||
|
:Штатное завершение;
|
||||||
|
stop
|
||||||
|
@enduml
|
||||||
BIN
Report/lab2/uml/lr2-selection-copy-paste.png
Normal file
BIN
Report/lab2/uml/lr2-selection-copy-paste.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
26
Report/lab2/uml/lr2-selection-copy-paste.puml
Normal file
26
Report/lab2/uml/lr2-selection-copy-paste.puml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
@startuml
|
||||||
|
title ЛР2 ИВ1: выделение, копирование и вставка
|
||||||
|
|
||||||
|
start
|
||||||
|
:Пользователь выбирает Select;
|
||||||
|
:Формирование прямоугольника выделения;
|
||||||
|
|
||||||
|
if (Copy/Cut?) then (да)
|
||||||
|
:Пройти выделенную область;
|
||||||
|
:Преобразовать индексы в RGBA;
|
||||||
|
:Сохранить ClipboardFragment;
|
||||||
|
if (Cut?) then (да)
|
||||||
|
:Очистить выделенную область (индекс 0);
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
if (Paste?) then (да)
|
||||||
|
:Показать "плавающий" фрагмент;
|
||||||
|
:Пользователь задаёт позицию;
|
||||||
|
:CommitPaste -> запись в слой;
|
||||||
|
:Прозрачные пиксели пропустить;
|
||||||
|
endif
|
||||||
|
|
||||||
|
:Обновить холст;
|
||||||
|
stop
|
||||||
|
@enduml
|
||||||
BIN
Report/lab2/uml/lr2-tools-and-fill.png
Normal file
BIN
Report/lab2/uml/lr2-tools-and-fill.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
24
Report/lab2/uml/lr2-tools-and-fill.puml
Normal file
24
Report/lab2/uml/lr2-tools-and-fill.puml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
@startuml
|
||||||
|
title ЛР2 ИВ1: обработка инструментов рисования
|
||||||
|
|
||||||
|
start
|
||||||
|
:Событие мыши в PixelCanvas;
|
||||||
|
:Преобразование координат экрана -> пиксель;
|
||||||
|
|
||||||
|
if (Инструмент == Brush?) then (да)
|
||||||
|
:Вычислить маску кисти (круг);
|
||||||
|
:Записать выбранный индекс цвета;
|
||||||
|
elseif (Инструмент == Eraser?) then (да)
|
||||||
|
:Вычислить маску кисти (круг);
|
||||||
|
:Записать индекс 0 (прозрачный);
|
||||||
|
elseif (Инструмент == Fill?) then (да)
|
||||||
|
:Запустить FloodFillService.Fill;
|
||||||
|
:BFS по 4-связным соседям;
|
||||||
|
:Перекрасить только область target-индекса;
|
||||||
|
else (Select)
|
||||||
|
:Передать управление блоку выделения;
|
||||||
|
endif
|
||||||
|
|
||||||
|
:Обновить итоговый буфер и холст;
|
||||||
|
stop
|
||||||
|
@enduml
|
||||||
4882
Report/lab2/zivro-lab2-report-with-code.md
Normal file
4882
Report/lab2/zivro-lab2-report-with-code.md
Normal file
File diff suppressed because it is too large
Load Diff
147
Report/lab2/zivro-lab2-report.md
Normal file
147
Report/lab2/zivro-lab2-report.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Лабораторная работа 2
|
||||||
|
## Способы и средства хранения и обработки графических данных
|
||||||
|
### Вариант ИВ1: разработка растрового редактора
|
||||||
|
|
||||||
|
## 1. Цель и практический результат
|
||||||
|
|
||||||
|
Цель работы - разработать растровый редактор, выполняющий создание, загрузку, редактирование и сохранение графического контейнера с пиксельными данными, а также реализовать базовые инструменты рисования и редактирования фрагментов.
|
||||||
|
|
||||||
|
Практический результат:
|
||||||
|
|
||||||
|
- разработано настольное приложение `Minint` на `C#` + `Avalonia`;
|
||||||
|
- реализован собственный бинарный формат контейнера `.minint` с чтением/записью;
|
||||||
|
- реализованы инструменты `Brush`, `Eraser`, `Fill`, `Select`, `Copy/Cut/Paste`;
|
||||||
|
- подготовлена документация с UML-диаграммами и приложением исходного кода.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 2. Соответствие варианту ИВ1 и замечание по структуре контейнера
|
||||||
|
|
||||||
|
По методическим указаниям ИВ1 требуется использовать структуру пикселя и контейнера из одного из вариантов `КВ1–КВ4`.
|
||||||
|
|
||||||
|
В текущем проекте фактически реализован палитровый контейнер с `RGBA`-палитрой и индексами пикселей (`MinintContainer`/`MinintDocument`/`MinintLayer`), что не совпадает буквально с описаниями `КВ1–КВ4`, но полностью закрывает функциональные требования ИВ1 (создание, редактирование, загрузка, сохранение, инструменты рисования, работа с фрагментами).
|
||||||
|
|
||||||
|
Это ограничение фиксируется в отчёте явно, чтобы не было расхождения между кодом и документацией.
|
||||||
|
|
||||||
|
## 3. Выполнение основных требований ИВ1
|
||||||
|
|
||||||
|
### 3.1 Создание, загрузка и сохранение контейнера
|
||||||
|
|
||||||
|
- создание нового контейнера выполняется через `EditorViewModel.NewContainer(...)`;
|
||||||
|
- загрузка/сохранение выполняется через `MinintSerializer` (собственная реализация чтения/записи);
|
||||||
|
- контейнер хранит общие размеры, набор документов (кадров), палитры и слои.
|
||||||
|
|
||||||
|
Реализация: `Minint/ViewModels/EditorViewModel.cs`, `Minint.Infrastructure/Serialization/MinintSerializer.cs`, `Minint.Core/Models/*`.
|
||||||
|
|
||||||
|
### 3.2 Редактирование единичных пикселей
|
||||||
|
|
||||||
|
- инструмент `Brush` изменяет значения пикселей маской радиуса;
|
||||||
|
- инструмент `Eraser` записывает индекс прозрачного цвета (`0`);
|
||||||
|
- выбор цвета выполняется через текущий `SelectedColor` и палитру документа.
|
||||||
|
|
||||||
|
Реализация: `Minint.Core/Services/Impl/DrawingService.cs`, `Minint/ViewModels/EditorViewModel.cs`.
|
||||||
|
|
||||||
|
### 3.3 Непрерывная отрисовка "кистью"
|
||||||
|
|
||||||
|
- при перемещении мыши по зажатой кнопке вызывается последовательная обработка точек;
|
||||||
|
- маска кисти вычисляется как круг по радиусу;
|
||||||
|
- поддерживается визуальный preview маски инструмента.
|
||||||
|
|
||||||
|
Реализация: `Minint/Controls/PixelCanvas.cs`, `Minint/Core/Services/Impl/DrawingService.cs`, `Minint/ViewModels/EditorViewModel.cs`.
|
||||||
|
|
||||||
|
### 3.4 Закраска области ("заливка")
|
||||||
|
|
||||||
|
- реализован алгоритм flood fill (4-связность);
|
||||||
|
- заливка ограничена областью одинакового исходного индекса;
|
||||||
|
- алгоритм работает в границах изображения.
|
||||||
|
|
||||||
|
Реализация: `Minint.Core/Services/Impl/FloodFillService.cs`.
|
||||||
|
|
||||||
|
### 3.5 Выделение, копирование/вырезание и вставка фрагмента
|
||||||
|
|
||||||
|
- реализована рамка выделения (`SelectionRect`);
|
||||||
|
- данные буфера обмена хранятся в palette-independent виде (`ClipboardFragment`, `RGBA`);
|
||||||
|
- вставка поддерживает предпросмотр и подтверждение позиции;
|
||||||
|
- прозрачные пиксели фрагмента при вставке пропускаются.
|
||||||
|
|
||||||
|
Реализация: `Minint/ViewModels/EditorViewModel.cs`, `Minint/Controls/PixelCanvas.cs`, `Minint.Core/Services/Impl/FragmentService.cs`.
|
||||||
|
|
||||||
|
## 4. Структура контейнера и пикселя
|
||||||
|
|
||||||
|
### 4.1 Структура контейнера `.minint`
|
||||||
|
|
||||||
|
Контейнер состоит из:
|
||||||
|
|
||||||
|
1. Заголовка файла:
|
||||||
|
- сигнатура `MININT`;
|
||||||
|
- версия формата;
|
||||||
|
- ширина и высота;
|
||||||
|
- количество документов;
|
||||||
|
- резерв.
|
||||||
|
2. Набора документов:
|
||||||
|
- имя документа;
|
||||||
|
- задержка кадра;
|
||||||
|
- палитра `RGBA`;
|
||||||
|
- набор слоёв.
|
||||||
|
3. Набора слоёв:
|
||||||
|
- имя слоя;
|
||||||
|
- признак видимости;
|
||||||
|
- непрозрачность;
|
||||||
|
- массив индексов пикселей.
|
||||||
|
|
||||||
|
### 4.2 Структура пикселя
|
||||||
|
|
||||||
|
Логически пиксель хранится как индекс в палитре документа (`int` в ОЗУ, переменная ширина 1..4 байта в файле), а итоговый цвет формируется по таблице `RgbaColor`.
|
||||||
|
|
||||||
|
## 5. Основные алгоритмы
|
||||||
|
|
||||||
|
1. Запись контейнера в бинарный поток (`WriteHeader`, `WriteDocument`, `WriteLayer`).
|
||||||
|
2. Чтение и валидация контейнера (`ReadHeader`, `ReadDocument`, `ReadLayer`).
|
||||||
|
3. Круглая кисть по маске радиуса.
|
||||||
|
4. Очистка пикселей ластиком.
|
||||||
|
5. Flood fill по очереди.
|
||||||
|
6. Копирование/вставка фрагмента с отсечением по границам.
|
||||||
|
7. Композиция слоёв при обновлении холста.
|
||||||
|
|
||||||
|
## 6. UML-диаграммы (PlantUML)
|
||||||
|
|
||||||
|
### 6.1 Основной рабочий цикл редактора
|
||||||
|
|
||||||
|
`Report/lab2/uml/lr2-editor-workflow.puml`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 6.2 Формат контейнера и сериализация
|
||||||
|
|
||||||
|
`Report/lab2/uml/lr2-container-serialization.puml`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 6.3 Инструменты рисования и заливки
|
||||||
|
|
||||||
|
`Report/lab2/uml/lr2-tools-and-fill.puml`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 6.4 Выделение и буфер обмена фрагментов
|
||||||
|
|
||||||
|
`Report/lab2/uml/lr2-selection-copy-paste.puml`
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 7. Проверка работоспособности
|
||||||
|
|
||||||
|
Для проверки корректности реализации используются модульные тесты проекта `Minint.Tests`:
|
||||||
|
|
||||||
|
- `DrawingTests`;
|
||||||
|
- `FloodFillTests`;
|
||||||
|
- `FragmentServiceTests`;
|
||||||
|
- `SerializerTests`;
|
||||||
|
- `CompositorTests`;
|
||||||
|
- `ExportTests`.
|
||||||
|
|
||||||
|
## 8. Вывод
|
||||||
|
|
||||||
|
В рамках ЛР2 (вариант ИВ1) реализовано рабочее приложение-растровый редактор с собственным контейнером данных и базовым набором инструментов редактирования изображения. Практические требования ИВ1 закрыты на уровне пользовательского сценария и программной реализации.
|
||||||
|
|
||||||
|
Отдельно зафиксировано, что выбранная структура контейнера является палитровой и не повторяет буквально формулировки `КВ1–КВ4`; при этом это не противоречит задаче разработки редактора и демонстрирует полноценную обработку графических данных.
|
||||||
BIN
Report/lab2/zivro-lab2-report.pdf
Normal file
BIN
Report/lab2/zivro-lab2-report.pdf
Normal file
Binary file not shown.
182
Report/render_uml_png.py
Normal file
182
Report/render_uml_png.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Render PlantUML diagrams (.puml) to high-resolution PNG files via Kroki.
|
||||||
|
|
||||||
|
Why this script:
|
||||||
|
- local `plantuml` may require X11 in some environments;
|
||||||
|
- this script uses Kroki HTTP API and can raise output quality via `skinparam dpi`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
python3 Report/render_uml_png.py
|
||||||
|
python3 Report/render_uml_png.py --dpi 360 --glob "*.puml"
|
||||||
|
python3 Report/render_uml_png.py --input-dir Report/uml --timeout 120
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import io
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Render .puml diagrams to PNG with configurable DPI."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--input-dir",
|
||||||
|
default="Report/uml",
|
||||||
|
help="Directory with .puml files (default: Report/uml)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--glob",
|
||||||
|
default="*.puml",
|
||||||
|
help="Glob pattern inside input-dir (default: *.puml)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dpi",
|
||||||
|
type=int,
|
||||||
|
default=300,
|
||||||
|
help="Output DPI via PlantUML skinparam (default: 300)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--timeout",
|
||||||
|
type=int,
|
||||||
|
default=120,
|
||||||
|
help="HTTP timeout in seconds (default: 120)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--kroki-url",
|
||||||
|
default="https://kroki.io/plantuml/png",
|
||||||
|
help="Kroki PlantUML PNG endpoint",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--min-dpi",
|
||||||
|
type=int,
|
||||||
|
default=96,
|
||||||
|
help="Lower bound for auto DPI fallback (default: 96)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-dim",
|
||||||
|
type=int,
|
||||||
|
default=3900,
|
||||||
|
help=(
|
||||||
|
"Target max PNG dimension during auto-fit. "
|
||||||
|
"Keep below backend hard limits (default: 3900)"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def iter_puml_files(input_dir: Path, pattern: str) -> Iterable[Path]:
|
||||||
|
return sorted(p for p in input_dir.glob(pattern) if p.is_file())
|
||||||
|
|
||||||
|
|
||||||
|
def inject_or_replace_skinparam(plantuml_text: str, key: str, value: int) -> str:
|
||||||
|
"""
|
||||||
|
Ensure diagram has the requested `skinparam <key>`.
|
||||||
|
- If skinparam exists, replace it.
|
||||||
|
- Otherwise insert after @startuml line.
|
||||||
|
"""
|
||||||
|
line_value = f"skinparam {key} {value}"
|
||||||
|
pattern = rf"(?im)^\s*skinparam\s+{re.escape(key)}\s+\d+\s*$"
|
||||||
|
|
||||||
|
if re.search(pattern, plantuml_text):
|
||||||
|
return re.sub(pattern, line_value, plantuml_text)
|
||||||
|
|
||||||
|
lines = plantuml_text.splitlines()
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if line.strip().lower().startswith("@startuml"):
|
||||||
|
lines.insert(i + 1, line_value)
|
||||||
|
return "\n".join(lines) + ("\n" if plantuml_text.endswith("\n") else "")
|
||||||
|
|
||||||
|
# Fallback: if @startuml is missing, prepend anyway.
|
||||||
|
return line_value + "\n" + plantuml_text
|
||||||
|
|
||||||
|
|
||||||
|
def render_one(
|
||||||
|
puml_file: Path,
|
||||||
|
kroki_url: str,
|
||||||
|
dpi: int,
|
||||||
|
min_dpi: int,
|
||||||
|
max_dim: int,
|
||||||
|
timeout: int,
|
||||||
|
) -> Path:
|
||||||
|
source = puml_file.read_text(encoding="utf-8")
|
||||||
|
source = inject_or_replace_skinparam(source, "padding", 24)
|
||||||
|
|
||||||
|
current_dpi = dpi
|
||||||
|
png_bytes: bytes | None = None
|
||||||
|
|
||||||
|
while True:
|
||||||
|
payload = inject_or_replace_skinparam(source, "dpi", current_dpi)
|
||||||
|
response = requests.post(
|
||||||
|
kroki_url,
|
||||||
|
data=payload.encode("utf-8"),
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
candidate = response.content
|
||||||
|
|
||||||
|
with Image.open(io.BytesIO(candidate)) as im:
|
||||||
|
width, height = im.size
|
||||||
|
|
||||||
|
# Kroki/PlantUML PNG responses may hit hard canvas limits.
|
||||||
|
# If we are close to that ceiling, lower DPI and retry.
|
||||||
|
if (width > max_dim or height > max_dim) and current_dpi > min_dpi:
|
||||||
|
next_dpi = max(min_dpi, int(current_dpi * 0.82))
|
||||||
|
if next_dpi == current_dpi:
|
||||||
|
next_dpi = current_dpi - 1
|
||||||
|
current_dpi = max(min_dpi, next_dpi)
|
||||||
|
continue
|
||||||
|
|
||||||
|
png_bytes = candidate
|
||||||
|
break
|
||||||
|
|
||||||
|
if png_bytes is None:
|
||||||
|
raise RuntimeError(f"Failed to render diagram: {puml_file}")
|
||||||
|
|
||||||
|
out_file = puml_file.with_suffix(".png")
|
||||||
|
out_file.write_bytes(png_bytes)
|
||||||
|
return out_file
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
|
||||||
|
input_dir = Path(args.input_dir).resolve()
|
||||||
|
if not input_dir.exists():
|
||||||
|
raise FileNotFoundError(f"Input directory not found: {input_dir}")
|
||||||
|
|
||||||
|
files = list(iter_puml_files(input_dir, args.glob))
|
||||||
|
if not files:
|
||||||
|
print(f"No files matched in {input_dir} with pattern {args.glob}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
print(f"Rendering {len(files)} diagrams from: {input_dir}")
|
||||||
|
print(f"DPI: {args.dpi}")
|
||||||
|
|
||||||
|
success = 0
|
||||||
|
for puml in files:
|
||||||
|
out = render_one(
|
||||||
|
puml_file=puml,
|
||||||
|
kroki_url=args.kroki_url,
|
||||||
|
dpi=args.dpi,
|
||||||
|
min_dpi=args.min_dpi,
|
||||||
|
max_dim=args.max_dim,
|
||||||
|
timeout=args.timeout,
|
||||||
|
)
|
||||||
|
print(f"created {out}")
|
||||||
|
success += 1
|
||||||
|
|
||||||
|
print(f"Done: {success}/{len(files)} PNG files generated.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
Reference in New Issue
Block a user