// Пакет 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 }