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:
118
image_table.go
118
image_table.go
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user