fix: исправление багов, комментарии и документация

- Исправлена двойная обработка ITableRow через reflection
- Исправлен выход за границы изображения при отрисовке последнего разделителя
- Добавлена защита от пустых данных (header, blocks)
- Добавлена compile-time проверка интерфейса ITableRow
- Переименован tablle_block_style.go → table_block_style.go
- Добавлены комментарии на русском ко всем функциям и типам
- Написана документация README.md с примерами использования
- Добавлен CLAUDE.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 20:30:29 +03:00
parent a4c5fc94b7
commit b8e9f20ec3
8 changed files with 347 additions and 89 deletions

View File

@@ -1,3 +1,4 @@
// Пакет image_table — библиотека для рендеринга таблиц данных в PNG-изображения.
package image_table
import (
@@ -14,23 +15,25 @@ import (
"golang.org/x/image/math/fixed"
)
// Встроенный шрифт JetBrains Mono для рендеринга текста
//
//go:embed assets/jb.ttf
var jbTTF []byte
// Шрифт для содержимого таблицы (10pt)
var globalFace font.Face
// ---- Цвета ----
// ---- Цвета таблицы ----
var (
headerBg = color.RGBA{0xD8, 0xEC, 0xFF, 255} // голубой фон заголовка
rowBg1 = color.RGBA{0xFF, 0xFF, 0xFF, 255} // белый фон строки 1
rowBg2 = color.RGBA{0xF7, 0xFA, 0xFC, 255} // мягкий фон строки 2
borderCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // рамка
dividerCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // линии
textCol = color.RGBA{0x34, 0x34, 0x32, 255} // тёплый тёмно-серый текст
rowBg1 = color.RGBA{0xFF, 0xFF, 0xFF, 255} // белый фон нечётных строк
rowBg2 = color.RGBA{0xF7, 0xFA, 0xFC, 255} // серо-голубой фон чётных строк
borderCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // цвет внешней рамки
dividerCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // цвет разделительных линий
textCol = color.RGBA{0x34, 0x34, 0x32, 255} // тёплый тёмно-серый цвет текста
)
// ---- Инициализация шрифта ----
// Инициализация шрифта при загрузке пакета
func init() {
f, err := opentype.Parse(jbTTF)
if err != nil {
@@ -47,22 +50,14 @@ func init() {
}
}
// ---- Утилиты ----
// toString преобразует любое значение в строку
func toString(v any) string {
return fmt.Sprintf("%v", v)
}
// rowToStrings преобразует строку таблицы в массив строк.
// Поддерживает: срезы, массивы, структуры и одиночные значения.
func rowToStrings(v any) []string {
if r, ok := v.(ITableRow); ok {
cells := r.GetCells()
out := make([]string, len(cells))
for i, c := range cells {
out[i] = toString(c)
}
return out
}
val := reflect.ValueOf(v)
switch val.Kind() {
@@ -84,6 +79,16 @@ func rowToStrings(v any) []string {
return []string{toString(v)}
}
// cellsToStrings преобразует []any ячейки ITableRow в []string
func cellsToStrings(cells []any) []string {
out := make([]string, len(cells))
for i, c := range cells {
out[i] = toString(c)
}
return out
}
// drawText рисует текст на изображении в указанной позиции (x, y — базовая линия)
func drawText(img *image.RGBA, x, y int, text string) {
d := &font.Drawer{
Dst: img,
@@ -94,11 +99,13 @@ func drawText(img *image.RGBA, x, y int, text string) {
d.DrawString(text)
}
// measureText возвращает ширину текста в пикселях
func measureText(s string) int {
d := &font.Drawer{Face: globalFace}
return d.MeasureString(s).Round()
}
// roundedRect рисует прямоугольник со скруглёнными углами
func roundedRect(img *image.RGBA, rect image.Rectangle, r int, col color.Color) {
w := rect.Dx()
h := rect.Dy()
@@ -116,12 +123,18 @@ func roundedRect(img *image.RGBA, rect image.Rectangle, r int, col color.Color)
}
}
// dist вычисляет евклидово расстояние между двумя точками
func dist(x1, y1, x2, y2 int) float64 {
return math.Hypot(float64(x1-x2), float64(y1-y2))
}
// ---- Основная функция ----
// DrawTableWarm рендерит таблицу в изображение.
//
// header — заголовки столбцов.
// rows — строки данных. Каждый элемент может быть:
// - []string, []any — обычный срез значений
// - struct — поля структуры станут ячейками
// - ITableRow (например *TableBlockStyle) — для задания фона строки
func DrawTableWarm(header []string, rows []any) image.Image {
padX := 8
padY := 10
@@ -130,9 +143,15 @@ func DrawTableWarm(header []string, rows []any) image.Image {
headerHeight := 32
colCount := len(header)
// Если нет столбцов — возвращаем пустое изображение 1x1
if colCount == 0 {
return image.NewRGBA(image.Rect(0, 0, 1, 1))
}
colWidths := make([]int, colCount)
// ---- Автоширина ----
// ---- Автоподбор ширины столбцов ----
for i, h := range header {
w := measureText(h)
@@ -142,7 +161,12 @@ func DrawTableWarm(header []string, rows []any) image.Image {
}
for _, r := range rows {
cells := rowToStrings(r)
var cells []string
if tr, ok := r.(ITableRow); ok {
cells = cellsToStrings(tr.GetCells())
} else {
cells = rowToStrings(r)
}
for i := 0; i < len(cells) && i < colCount; i++ {
w := measureText(cells[i])
if w > colWidths[i] {
@@ -151,10 +175,12 @@ func DrawTableWarm(header []string, rows []any) image.Image {
}
}
// Добавляем внутренние отступы к ширине столбцов
for i := range colWidths {
colWidths[i] += padX * 2
}
// Подсчёт общих размеров изображения
imgWidth := 0
for _, w := range colWidths {
imgWidth += w
@@ -164,10 +190,10 @@ func DrawTableWarm(header []string, rows []any) image.Image {
img := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))
// общий фон
// Заливка общего фона
draw.Draw(img, img.Bounds(), &image.Uniform{rowBg1}, image.Point{}, draw.Src)
// ---- Заголовок ----
// ---- Отрисовка заголовка ----
roundedRect(img, image.Rect(0, 0, imgWidth, headerHeight), 8, headerBg)
// Горизонтальная линия под заголовком
@@ -180,59 +206,63 @@ func DrawTableWarm(header []string, rows []any) image.Image {
drawText(img, x+padX, headerHeight-padY, h)
x += colWidths[i]
// Вертикальный разделитель
for yy := 0; yy < headerHeight; yy++ {
img.Set(x, yy, dividerCol)
// Вертикальный разделитель между столбцами (не после последнего)
if i < colCount-1 {
for yy := 0; yy < headerHeight; yy++ {
img.Set(x, yy, dividerCol)
}
}
}
// ---- Строки ----
// ---- Отрисовка строк данных ----
y := headerHeight
for rowIndex, row := range rows {
// Чередование фона: нечётные строки — белые, чётные — серо-голубые
bg := rowBg1
if rowIndex%2 == 1 {
bg = rowBg2
}
sCells := row
switch rt := row.(type) {
case ITableRow:
bg = rt.GetBackgroundColor()
sCells = rt.GetCells()
// Получение ячеек и (опционально) пользовательского цвета фона
var cells []string
if tr, ok := row.(ITableRow); ok {
bg = tr.GetBackgroundColor()
cells = cellsToStrings(tr.GetCells())
} else {
cells = rowToStrings(row)
}
// фон строки
// Заливка фона строки
for xx := 0; xx < imgWidth; xx++ {
for yy := 0; yy < rowHeight; yy++ {
img.Set(xx, y+yy, bg)
}
}
// горизонтальный разделитель ВВЕРХУ строки
// Горизонтальный разделитель вверху строки
for xx := 0; xx < imgWidth; xx++ {
img.Set(xx, y, dividerCol)
}
cells := rowToStrings(sCells)
// Отрисовка текста ячеек
x = 0
for i := 0; i < colCount && i < len(cells); i++ {
drawText(img, x+padX, y+rowHeight-padY, cells[i])
x += colWidths[i]
// вертикальная линия
for yy := 0; yy < rowHeight; yy++ {
img.Set(x, y+yy, dividerCol)
// Вертикальный разделитель между столбцами (не после последнего)
if i < colCount-1 {
for yy := 0; yy < rowHeight; yy++ {
img.Set(x, y+yy, dividerCol)
}
}
}
y += rowHeight
}
// Рамка
// ---- Внешняя рамка таблицы ----
for xx := 0; xx < imgWidth; xx++ {
img.Set(xx, 0, borderCol)
img.Set(xx, imgHeight-1, borderCol)