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:
34
CLAUDE.md
Normal file
34
CLAUDE.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Go library for rendering data tables as PNG images. Provides two APIs:
|
||||||
|
- `DrawTableWarm()` — renders a single table with header and rows
|
||||||
|
- `RenderDocument()` — renders a multi-block document with titled table sections
|
||||||
|
|
||||||
|
## Build & Test Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go test ./... # run all tests
|
||||||
|
go test -run TestDrawTable # run single test
|
||||||
|
go test -run TestDocument # run document rendering test
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests output PNG files to `test-data/` (gitignored). Verify rendering by inspecting the generated images visually.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
- **`image_table.go`** — Core rendering engine. `DrawTableWarm()` draws a single table image. Contains font initialization (`init()`), text measurement/drawing utilities, rounded rect helper, and row-to-string conversion via reflection. Embeds `assets/jb.ttf` (JetBrains font) at compile time.
|
||||||
|
- **`document.go`** — `RenderDocument()` composes multiple `TableBlock`s into a single document image with titles, using a larger font face for block headings.
|
||||||
|
- **`table_block.go`** — `TableBlock` struct: a titled table with header and rows.
|
||||||
|
- **`tablle_block_style.go`** — `TableBlockStyle` implements `ITableRow` to allow per-row background colors.
|
||||||
|
- **`itablerow.go`** — `ITableRow` interface for custom row rendering (cells + background color).
|
||||||
|
|
||||||
|
## Key Patterns
|
||||||
|
|
||||||
|
- Rows accept `any` type: slices, structs (via reflection), or `ITableRow` implementors for custom styling.
|
||||||
|
- Column widths are auto-calculated from content measurement.
|
||||||
|
- Two `init()` functions parse the embedded TTF font at different sizes (10pt for table cells, 12pt for document titles).
|
||||||
|
- Comments and identifiers are in Russian.
|
||||||
173
README.md
173
README.md
@@ -1,3 +1,174 @@
|
|||||||
# image_table
|
# image_table
|
||||||
|
|
||||||
Создание изображения с таблицами данных
|
Go-библиотека для рендеринга таблиц данных в PNG-изображения. Подходит для генерации отчётов, уведомлений в мессенджерах и визуализации табличных данных.
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get git.gm6.ru/icewind/image_table
|
||||||
|
```
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
|
||||||
|
- Рендеринг одиночной таблицы с автоподбором ширины столбцов
|
||||||
|
- Рендеринг документа из нескольких именованных таблиц
|
||||||
|
- Поддержка различных типов данных в строках: срезы, структуры, интерфейс `ITableRow`
|
||||||
|
- Настройка цвета фона для отдельных строк через `TableBlockStyle`
|
||||||
|
- Встроенный шрифт JetBrains Mono — не требует внешних зависимостей для шрифтов
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
### Простая таблица
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/png"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.gm6.ru/icewind/image_table"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
header := []string{"Имя", "Возраст", "Город"}
|
||||||
|
|
||||||
|
rows := []any{
|
||||||
|
[]string{"Алиса", "23", "Москва"},
|
||||||
|
[]string{"Боб", "31", "Казань"},
|
||||||
|
[]string{"Олег", "44", "Сочи"},
|
||||||
|
}
|
||||||
|
|
||||||
|
img := image_table.DrawTableWarm(header, rows)
|
||||||
|
|
||||||
|
file, _ := os.Create("table.png")
|
||||||
|
defer file.Close()
|
||||||
|
png.Encode(file, img)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Строки из структур
|
||||||
|
|
||||||
|
Поля структуры автоматически преобразуются в ячейки таблицы:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type User struct {
|
||||||
|
Name string
|
||||||
|
Age int
|
||||||
|
City string
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := []any{
|
||||||
|
User{"Елена", 27, "Минск"},
|
||||||
|
User{"Иван", 35, "Киев"},
|
||||||
|
}
|
||||||
|
|
||||||
|
img := image_table.DrawTableWarm(
|
||||||
|
[]string{"Имя", "Возраст", "Город"},
|
||||||
|
rows,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Строки с пользовательским фоном
|
||||||
|
|
||||||
|
Используйте `TableBlockStyle` для задания цвета фона строки:
|
||||||
|
|
||||||
|
```go
|
||||||
|
rows := []any{
|
||||||
|
[]any{1, "Запрос обработан", "OK"},
|
||||||
|
&image_table.TableBlockStyle{
|
||||||
|
Cells: []any{2, "Ошибка соединения", "FAIL"},
|
||||||
|
BackgroundColor: color.RGBA{R: 255, G: 225, B: 225, A: 255}, // красный фон
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
img := image_table.DrawTableWarm(
|
||||||
|
[]string{"ID", "Описание", "Статус"},
|
||||||
|
rows,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Важно:** `TableBlockStyle` нужно передавать как указатель (`&TableBlockStyle{...}`), иначе интерфейс `ITableRow` не будет распознан.
|
||||||
|
|
||||||
|
### Документ из нескольких таблиц
|
||||||
|
|
||||||
|
`Document` позволяет объединить несколько таблиц с заголовками в одно изображение:
|
||||||
|
|
||||||
|
```go
|
||||||
|
doc := image_table.Document{
|
||||||
|
Blocks: []image_table.TableBlock{
|
||||||
|
{
|
||||||
|
Title: "Пользователи",
|
||||||
|
Header: []string{"ID", "Имя", "Возраст"},
|
||||||
|
Rows: []any{
|
||||||
|
[]any{1, "Иван", 30},
|
||||||
|
[]any{2, "Мария", 25},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Title: "Статистика",
|
||||||
|
Header: []string{"Метрика", "Значение"},
|
||||||
|
Rows: []any{
|
||||||
|
[]any{"Запросы", 12000},
|
||||||
|
&image_table.TableBlockStyle{
|
||||||
|
Cells: []any{"Ошибки", 37},
|
||||||
|
BackgroundColor: color.RGBA{R: 255, G: 225, B: 225, A: 255},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
img := image_table.RenderDocument(doc)
|
||||||
|
|
||||||
|
file, _ := os.Create("document.png")
|
||||||
|
defer file.Close()
|
||||||
|
png.Encode(file, img)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Собственная реализация ITableRow
|
||||||
|
|
||||||
|
Для полного контроля над строкой реализуйте интерфейс `ITableRow`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type AlertRow struct {
|
||||||
|
Level string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AlertRow) GetCells() []any {
|
||||||
|
return []any{r.Level, r.Message}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AlertRow) GetBackgroundColor() color.RGBA {
|
||||||
|
if r.Level == "CRITICAL" {
|
||||||
|
return color.RGBA{R: 255, G: 200, B: 200, A: 255}
|
||||||
|
}
|
||||||
|
return color.RGBA{R: 255, G: 255, B: 255, A: 255}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
### `DrawTableWarm(header []string, rows []any) image.Image`
|
||||||
|
|
||||||
|
Рендерит одну таблицу. Строки могут быть `[]string`, `[]any`, структурой или реализацией `ITableRow`.
|
||||||
|
|
||||||
|
### `RenderDocument(doc Document) image.Image`
|
||||||
|
|
||||||
|
Рендерит документ из нескольких таблиц (`Document.Blocks`), каждая с заголовком.
|
||||||
|
|
||||||
|
### `ITableRow`
|
||||||
|
|
||||||
|
```go
|
||||||
|
type ITableRow interface {
|
||||||
|
GetCells() []any
|
||||||
|
GetBackgroundColor() color.RGBA
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Интерфейс для строк с пользовательским стилем. Готовая реализация — `TableBlockStyle`.
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
62
document.go
62
document.go
@@ -10,31 +10,24 @@ import (
|
|||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Document — документ, состоящий из нескольких таблиц с заголовками
|
||||||
type Document struct {
|
type Document struct {
|
||||||
Blocks []TableBlock
|
Blocks []TableBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Цвет заголовка блока
|
||||||
var titleColor = color.RGBA{0x22, 0x22, 0x20, 255}
|
var titleColor = color.RGBA{0x22, 0x22, 0x20, 255}
|
||||||
|
|
||||||
// Высота заголовка блока
|
// Высота заголовка блока (над таблицей)
|
||||||
const blockTitleHeight = 30
|
const blockTitleHeight = 30
|
||||||
|
|
||||||
|
// Вертикальный отступ между блоками
|
||||||
const blockSpacing = 20
|
const blockSpacing = 20
|
||||||
|
|
||||||
// Рисуем текст заголовка
|
// Шрифт для заголовков блоков (12pt)
|
||||||
func drawBigText(img *image.RGBA, x, y int, text string) {
|
|
||||||
d := &font.Drawer{
|
|
||||||
Dst: img,
|
|
||||||
Src: image.NewUniform(titleColor),
|
|
||||||
Face: globalFaceBig, // увеличенный шрифт
|
|
||||||
Dot: fixed.P(x, y),
|
|
||||||
}
|
|
||||||
d.DrawString(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Увеличенный шрифт для заголовков ----
|
|
||||||
|
|
||||||
var globalFaceBig font.Face
|
var globalFaceBig font.Face
|
||||||
|
|
||||||
|
// Инициализация увеличенного шрифта для заголовков блоков
|
||||||
func init() {
|
func init() {
|
||||||
f, err := opentype.Parse(jbTTF)
|
f, err := opentype.Parse(jbTTF)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,9 +44,26 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func RenderDocument(doc Document) image.Image {
|
// drawBigText рисует текст увеличенным шрифтом (для заголовков блоков)
|
||||||
// Сначала считаем общие размеры
|
func drawBigText(img *image.RGBA, x, y int, text string) {
|
||||||
|
d := &font.Drawer{
|
||||||
|
Dst: img,
|
||||||
|
Src: image.NewUniform(titleColor),
|
||||||
|
Face: globalFaceBig,
|
||||||
|
Dot: fixed.P(x, y),
|
||||||
|
}
|
||||||
|
d.DrawString(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderDocument рендерит документ — несколько именованных таблиц на одном изображении.
|
||||||
|
// Каждый блок содержит заголовок и таблицу. Блоки располагаются вертикально с отступами.
|
||||||
|
func RenderDocument(doc Document) image.Image {
|
||||||
|
// Если нет блоков — возвращаем пустое изображение
|
||||||
|
if len(doc.Blocks) == 0 {
|
||||||
|
return image.NewRGBA(image.Rect(0, 0, 1, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рендерим каждую таблицу отдельно и считаем общие размеры
|
||||||
width := 0
|
width := 0
|
||||||
height := 0
|
height := 0
|
||||||
|
|
||||||
@@ -71,26 +81,28 @@ func RenderDocument(doc Document) image.Image {
|
|||||||
height += blockTitleHeight + tableImg.Bounds().Dy() + blockSpacing
|
height += blockTitleHeight + tableImg.Bounds().Dy() + blockSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
// Создаём документ
|
// Внешние отступы документа
|
||||||
img := image.NewRGBA(image.Rect(0, 0, width+40, height+40))
|
margin := 20
|
||||||
|
|
||||||
// общий фон
|
// Создаём итоговое изображение
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, width+margin*2, height+margin*2))
|
||||||
|
|
||||||
|
// Белый фон документа
|
||||||
draw.Draw(img, img.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
|
draw.Draw(img, img.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
|
||||||
|
|
||||||
y := 20
|
y := margin
|
||||||
|
|
||||||
// рендерим блоки
|
// Отрисовка блоков
|
||||||
for i, block := range doc.Blocks {
|
for i, block := range doc.Blocks {
|
||||||
|
|
||||||
// Заголовок блока
|
// Заголовок блока
|
||||||
drawBigText(img, 20, y+blockTitleHeight-10, block.Title)
|
drawBigText(img, margin, y+blockTitleHeight-10, block.Title)
|
||||||
y += blockTitleHeight
|
y += blockTitleHeight
|
||||||
|
|
||||||
// Таблица
|
// Таблица блока
|
||||||
tableImg := blockImages[i]
|
tableImg := blockImages[i]
|
||||||
draw.Draw(
|
draw.Draw(
|
||||||
img,
|
img,
|
||||||
image.Rect(20, y, 20+tableImg.Bounds().Dx(), y+tableImg.Bounds().Dy()),
|
image.Rect(margin, y, margin+tableImg.Bounds().Dx(), y+tableImg.Bounds().Dy()),
|
||||||
tableImg,
|
tableImg,
|
||||||
image.Point{},
|
image.Point{},
|
||||||
draw.Over,
|
draw.Over,
|
||||||
|
|||||||
110
image_table.go
110
image_table.go
@@ -1,3 +1,4 @@
|
|||||||
|
// Пакет image_table — библиотека для рендеринга таблиц данных в PNG-изображения.
|
||||||
package image_table
|
package image_table
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -14,23 +15,25 @@ import (
|
|||||||
"golang.org/x/image/math/fixed"
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Встроенный шрифт JetBrains Mono для рендеринга текста
|
||||||
|
//
|
||||||
//go:embed assets/jb.ttf
|
//go:embed assets/jb.ttf
|
||||||
var jbTTF []byte
|
var jbTTF []byte
|
||||||
|
|
||||||
|
// Шрифт для содержимого таблицы (10pt)
|
||||||
var globalFace font.Face
|
var globalFace font.Face
|
||||||
|
|
||||||
// ---- Цвета ----
|
// ---- Цвета таблицы ----
|
||||||
var (
|
var (
|
||||||
headerBg = color.RGBA{0xD8, 0xEC, 0xFF, 255} // голубой фон заголовка
|
headerBg = color.RGBA{0xD8, 0xEC, 0xFF, 255} // голубой фон заголовка
|
||||||
rowBg1 = color.RGBA{0xFF, 0xFF, 0xFF, 255} // белый фон строки 1
|
rowBg1 = color.RGBA{0xFF, 0xFF, 0xFF, 255} // белый фон нечётных строк
|
||||||
rowBg2 = color.RGBA{0xF7, 0xFA, 0xFC, 255} // мягкий фон строки 2
|
rowBg2 = color.RGBA{0xF7, 0xFA, 0xFC, 255} // серо-голубой фон чётных строк
|
||||||
borderCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // рамка
|
borderCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // цвет внешней рамки
|
||||||
dividerCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // линии
|
dividerCol = color.RGBA{0xB8, 0xC8, 0xD8, 255} // цвет разделительных линий
|
||||||
textCol = color.RGBA{0x34, 0x34, 0x32, 255} // тёплый тёмно-серый текст
|
textCol = color.RGBA{0x34, 0x34, 0x32, 255} // тёплый тёмно-серый цвет текста
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---- Инициализация шрифта ----
|
// Инициализация шрифта при загрузке пакета
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
f, err := opentype.Parse(jbTTF)
|
f, err := opentype.Parse(jbTTF)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,22 +50,14 @@ func init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Утилиты ----
|
// toString преобразует любое значение в строку
|
||||||
|
|
||||||
func toString(v any) string {
|
func toString(v any) string {
|
||||||
return fmt.Sprintf("%v", v)
|
return fmt.Sprintf("%v", v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rowToStrings преобразует строку таблицы в массив строк.
|
||||||
|
// Поддерживает: срезы, массивы, структуры и одиночные значения.
|
||||||
func rowToStrings(v any) []string {
|
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)
|
val := reflect.ValueOf(v)
|
||||||
|
|
||||||
switch val.Kind() {
|
switch val.Kind() {
|
||||||
@@ -84,6 +79,16 @@ func rowToStrings(v any) []string {
|
|||||||
return []string{toString(v)}
|
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) {
|
func drawText(img *image.RGBA, x, y int, text string) {
|
||||||
d := &font.Drawer{
|
d := &font.Drawer{
|
||||||
Dst: img,
|
Dst: img,
|
||||||
@@ -94,11 +99,13 @@ func drawText(img *image.RGBA, x, y int, text string) {
|
|||||||
d.DrawString(text)
|
d.DrawString(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// measureText возвращает ширину текста в пикселях
|
||||||
func measureText(s string) int {
|
func measureText(s string) int {
|
||||||
d := &font.Drawer{Face: globalFace}
|
d := &font.Drawer{Face: globalFace}
|
||||||
return d.MeasureString(s).Round()
|
return d.MeasureString(s).Round()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// roundedRect рисует прямоугольник со скруглёнными углами
|
||||||
func roundedRect(img *image.RGBA, rect image.Rectangle, r int, col color.Color) {
|
func roundedRect(img *image.RGBA, rect image.Rectangle, r int, col color.Color) {
|
||||||
w := rect.Dx()
|
w := rect.Dx()
|
||||||
h := rect.Dy()
|
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 {
|
func dist(x1, y1, x2, y2 int) float64 {
|
||||||
return math.Hypot(float64(x1-x2), float64(y1-y2))
|
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 {
|
func DrawTableWarm(header []string, rows []any) image.Image {
|
||||||
padX := 8
|
padX := 8
|
||||||
padY := 10
|
padY := 10
|
||||||
@@ -130,9 +143,15 @@ func DrawTableWarm(header []string, rows []any) image.Image {
|
|||||||
headerHeight := 32
|
headerHeight := 32
|
||||||
|
|
||||||
colCount := len(header)
|
colCount := len(header)
|
||||||
|
|
||||||
|
// Если нет столбцов — возвращаем пустое изображение 1x1
|
||||||
|
if colCount == 0 {
|
||||||
|
return image.NewRGBA(image.Rect(0, 0, 1, 1))
|
||||||
|
}
|
||||||
|
|
||||||
colWidths := make([]int, colCount)
|
colWidths := make([]int, colCount)
|
||||||
|
|
||||||
// ---- Автоширина ----
|
// ---- Автоподбор ширины столбцов ----
|
||||||
|
|
||||||
for i, h := range header {
|
for i, h := range header {
|
||||||
w := measureText(h)
|
w := measureText(h)
|
||||||
@@ -142,7 +161,12 @@ func DrawTableWarm(header []string, rows []any) image.Image {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range rows {
|
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++ {
|
for i := 0; i < len(cells) && i < colCount; i++ {
|
||||||
w := measureText(cells[i])
|
w := measureText(cells[i])
|
||||||
if w > colWidths[i] {
|
if w > colWidths[i] {
|
||||||
@@ -151,10 +175,12 @@ func DrawTableWarm(header []string, rows []any) image.Image {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем внутренние отступы к ширине столбцов
|
||||||
for i := range colWidths {
|
for i := range colWidths {
|
||||||
colWidths[i] += padX * 2
|
colWidths[i] += padX * 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Подсчёт общих размеров изображения
|
||||||
imgWidth := 0
|
imgWidth := 0
|
||||||
for _, w := range colWidths {
|
for _, w := range colWidths {
|
||||||
imgWidth += w
|
imgWidth += w
|
||||||
@@ -164,10 +190,10 @@ func DrawTableWarm(header []string, rows []any) image.Image {
|
|||||||
|
|
||||||
img := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))
|
img := image.NewRGBA(image.Rect(0, 0, imgWidth, imgHeight))
|
||||||
|
|
||||||
// общий фон
|
// Заливка общего фона
|
||||||
draw.Draw(img, img.Bounds(), &image.Uniform{rowBg1}, image.Point{}, draw.Src)
|
draw.Draw(img, img.Bounds(), &image.Uniform{rowBg1}, image.Point{}, draw.Src)
|
||||||
|
|
||||||
// ---- Заголовок ----
|
// ---- Отрисовка заголовка ----
|
||||||
roundedRect(img, image.Rect(0, 0, imgWidth, headerHeight), 8, headerBg)
|
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)
|
drawText(img, x+padX, headerHeight-padY, h)
|
||||||
x += colWidths[i]
|
x += colWidths[i]
|
||||||
|
|
||||||
// Вертикальный разделитель
|
// Вертикальный разделитель между столбцами (не после последнего)
|
||||||
|
if i < colCount-1 {
|
||||||
for yy := 0; yy < headerHeight; yy++ {
|
for yy := 0; yy < headerHeight; yy++ {
|
||||||
img.Set(x, yy, dividerCol)
|
img.Set(x, yy, dividerCol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Строки ----
|
// ---- Отрисовка строк данных ----
|
||||||
y := headerHeight
|
y := headerHeight
|
||||||
for rowIndex, row := range rows {
|
for rowIndex, row := range rows {
|
||||||
|
// Чередование фона: нечётные строки — белые, чётные — серо-голубые
|
||||||
bg := rowBg1
|
bg := rowBg1
|
||||||
|
|
||||||
if rowIndex%2 == 1 {
|
if rowIndex%2 == 1 {
|
||||||
bg = rowBg2
|
bg = rowBg2
|
||||||
}
|
}
|
||||||
|
|
||||||
sCells := row
|
// Получение ячеек и (опционально) пользовательского цвета фона
|
||||||
|
var cells []string
|
||||||
switch rt := row.(type) {
|
if tr, ok := row.(ITableRow); ok {
|
||||||
case ITableRow:
|
bg = tr.GetBackgroundColor()
|
||||||
bg = rt.GetBackgroundColor()
|
cells = cellsToStrings(tr.GetCells())
|
||||||
sCells = rt.GetCells()
|
} else {
|
||||||
|
cells = rowToStrings(row)
|
||||||
}
|
}
|
||||||
|
|
||||||
// фон строки
|
// Заливка фона строки
|
||||||
for xx := 0; xx < imgWidth; xx++ {
|
for xx := 0; xx < imgWidth; xx++ {
|
||||||
for yy := 0; yy < rowHeight; yy++ {
|
for yy := 0; yy < rowHeight; yy++ {
|
||||||
img.Set(xx, y+yy, bg)
|
img.Set(xx, y+yy, bg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// горизонтальный разделитель ВВЕРХУ строки
|
// Горизонтальный разделитель вверху строки
|
||||||
for xx := 0; xx < imgWidth; xx++ {
|
for xx := 0; xx < imgWidth; xx++ {
|
||||||
img.Set(xx, y, dividerCol)
|
img.Set(xx, y, dividerCol)
|
||||||
}
|
}
|
||||||
|
|
||||||
cells := rowToStrings(sCells)
|
// Отрисовка текста ячеек
|
||||||
|
|
||||||
x = 0
|
x = 0
|
||||||
for i := 0; i < colCount && i < len(cells); i++ {
|
for i := 0; i < colCount && i < len(cells); i++ {
|
||||||
drawText(img, x+padX, y+rowHeight-padY, cells[i])
|
drawText(img, x+padX, y+rowHeight-padY, cells[i])
|
||||||
|
|
||||||
x += colWidths[i]
|
x += colWidths[i]
|
||||||
|
|
||||||
// вертикальная линия
|
// Вертикальный разделитель между столбцами (не после последнего)
|
||||||
|
if i < colCount-1 {
|
||||||
for yy := 0; yy < rowHeight; yy++ {
|
for yy := 0; yy < rowHeight; yy++ {
|
||||||
img.Set(x, y+yy, dividerCol)
|
img.Set(x, y+yy, dividerCol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
y += rowHeight
|
y += rowHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Рамка
|
// ---- Внешняя рамка таблицы ----
|
||||||
for xx := 0; xx < imgWidth; xx++ {
|
for xx := 0; xx < imgWidth; xx++ {
|
||||||
img.Set(xx, 0, borderCol)
|
img.Set(xx, 0, borderCol)
|
||||||
img.Set(xx, imgHeight-1, borderCol)
|
img.Set(xx, imgHeight-1, borderCol)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package image_table
|
|||||||
|
|
||||||
import "image/color"
|
import "image/color"
|
||||||
|
|
||||||
|
// ITableRow — интерфейс для строки таблицы с пользовательским стилем.
|
||||||
|
// Позволяет задать ячейки и цвет фона строки.
|
||||||
type ITableRow interface {
|
type ITableRow interface {
|
||||||
GetCells() []any
|
GetCells() []any
|
||||||
GetBackgroundColor() color.RGBA
|
GetBackgroundColor() color.RGBA
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
package image_table
|
package image_table
|
||||||
|
|
||||||
|
// TableBlock — блок документа: именованная таблица с заголовком и строками данных
|
||||||
type TableBlock struct {
|
type TableBlock struct {
|
||||||
Title string
|
Title string // заголовок блока (отображается над таблицей)
|
||||||
Header []string
|
Header []string // заголовки столбцов
|
||||||
Rows []any
|
Rows []any // строки данных ([]string, []any, struct или ITableRow)
|
||||||
}
|
}
|
||||||
|
|||||||
24
table_block_style.go
Normal file
24
table_block_style.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package image_table
|
||||||
|
|
||||||
|
import "image/color"
|
||||||
|
|
||||||
|
// Проверка реализации интерфейса на этапе компиляции
|
||||||
|
var _ ITableRow = (*TableBlockStyle)(nil)
|
||||||
|
|
||||||
|
// TableBlockStyle — строка таблицы с пользовательским цветом фона.
|
||||||
|
// Реализует интерфейс ITableRow.
|
||||||
|
// Важно: передавать в rows как указатель (*TableBlockStyle), иначе интерфейс не будет распознан.
|
||||||
|
type TableBlockStyle struct {
|
||||||
|
Cells []any // ячейки строки
|
||||||
|
BackgroundColor color.RGBA // цвет фона строки
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCells возвращает ячейки строки
|
||||||
|
func (s *TableBlockStyle) GetCells() []any {
|
||||||
|
return s.Cells
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBackgroundColor возвращает цвет фона строки
|
||||||
|
func (s *TableBlockStyle) GetBackgroundColor() color.RGBA {
|
||||||
|
return s.BackgroundColor
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package image_table
|
|
||||||
|
|
||||||
import "image/color"
|
|
||||||
|
|
||||||
type TableBlockStyle struct {
|
|
||||||
Cells []any
|
|
||||||
BackgroundColor color.RGBA
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TableBlockStyle) GetCells() []any {
|
|
||||||
return s.Cells
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *TableBlockStyle) GetBackgroundColor() color.RGBA {
|
|
||||||
return s.BackgroundColor
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user