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" ) //go:embed assets/jb.ttf var jbTTF []byte 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} // тёплый тёмно-серый текст ) // ---- Инициализация шрифта ---- 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) } } // ---- Утилиты ---- func toString(v any) string { return fmt.Sprintf("%v", v) } 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)} } 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) } func measureText(s string) int { d := &font.Drawer{Face: globalFace} return d.MeasureString(s).Round() } 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) } } } func dist(x1, y1, x2, y2 int) float64 { return math.Hypot(float64(x1-x2), float64(y1-y2)) } // ---- Основная функция ---- func DrawTableWarm(header []string, rows []any) image.Image { padX := 8 padY := 10 rowHeight := 26 headerHeight := 32 colCount := len(header) colWidths := make([]int, colCount) // ---- Автоширина ---- for i, h := range header { w := measureText(h) if w > colWidths[i] { colWidths[i] = w } } for _, r := range rows { 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] // Вертикальный разделитель 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 } // фон строки 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(row) 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) } } 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 }