Files
image_table/image_table.go
Maksimov V Vladimir b8e9f20ec3 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>
2026-04-01 20:30:54 +03:00

277 lines
7.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Пакет image_table — библиотека для рендеринга таблиц данных в PNG-изображения.
package image_table
import (
_ "embed"
"fmt"
"image"
"image/color"
"image/draw"
"math"
"reflect"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"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} // белый фон нечётных строк
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 {
panic(err)
}
globalFace, err = opentype.NewFace(f, &opentype.FaceOptions{
Size: 10,
DPI: 96,
Hinting: font.HintingFull,
})
if err != nil {
panic(err)
}
}
// toString преобразует любое значение в строку
func toString(v any) string {
return fmt.Sprintf("%v", v)
}
// rowToStrings преобразует строку таблицы в массив строк.
// Поддерживает: срезы, массивы, структуры и одиночные значения.
func rowToStrings(v any) []string {
val := reflect.ValueOf(v)
switch val.Kind() {
case reflect.Slice, reflect.Array:
out := make([]string, val.Len())
for i := 0; i < val.Len(); i++ {
out[i] = toString(val.Index(i).Interface())
}
return out
case reflect.Struct:
out := make([]string, val.NumField())
for i := 0; i < val.NumField(); i++ {
out[i] = toString(val.Field(i).Interface())
}
return out
}
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,
Src: image.NewUniform(textCol),
Face: globalFace,
Dot: fixed.P(x, y),
}
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()
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
if (x < r && y < r && dist(x, y, r, r) > float64(r)) ||
(x >= w-r && y < r && dist(x, y, w-r-1, r) > float64(r)) ||
(x < r && y >= h-r && dist(x, y, r, h-r-1) > float64(r)) ||
(x >= w-r && y >= h-r && dist(x, y, w-r-1, h-r-1) > float64(r)) {
continue
}
img.Set(rect.Min.X+x, rect.Min.Y+y, col)
}
}
}
// 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
rowHeight := 26
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)
if w > colWidths[i] {
colWidths[i] = w
}
}
for _, r := range rows {
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] {
colWidths[i] = w
}
}
}
// Добавляем внутренние отступы к ширине столбцов
for i := range colWidths {
colWidths[i] += padX * 2
}
// Подсчёт общих размеров изображения
imgWidth := 0
for _, w := range colWidths {
imgWidth += w
}
imgHeight := headerHeight + len(rows)*rowHeight
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)
// Горизонтальная линия под заголовком
for xx := 0; xx < imgWidth; xx++ {
img.Set(xx, headerHeight-1, dividerCol)
}
x := 0
for i, h := range header {
drawText(img, x+padX, headerHeight-padY, h)
x += colWidths[i]
// Вертикальный разделитель между столбцами (не после последнего)
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
}
// Получение ячеек и (опционально) пользовательского цвета фона
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)
}
// Отрисовка текста ячеек
x = 0
for i := 0; i < colCount && i < len(cells); i++ {
drawText(img, x+padX, y+rowHeight-padY, cells[i])
x += colWidths[i]
// Вертикальный разделитель между столбцами (не после последнего)
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)
}
for yy := 0; yy < imgHeight; yy++ {
img.Set(0, yy, borderCol)
img.Set(imgWidth-1, yy, borderCol)
}
return img
}