github.com/laof/lite-speed-test@v0.0.0-20230930011949-1f39b7037845/web/render/table.go (about) 1 package render 2 3 import ( 4 "bytes" 5 "encoding/base64" 6 "encoding/csv" 7 "encoding/json" 8 "fmt" 9 "os" 10 "reflect" 11 "runtime" 12 "sort" 13 "strconv" 14 "time" 15 16 "github.com/laof/lite-speed-test/constant" 17 "github.com/laof/lite-speed-test/download" 18 "golang.org/x/image/font" 19 ) 20 21 type Theme struct { 22 colorgroup [][]int 23 bounds []int 24 } 25 26 var ( 27 themes = map[string]Theme{ 28 "original": Theme{ 29 colorgroup: [][]int{ 30 {255, 255, 255}, 31 {128, 255, 0}, 32 {255, 255, 0}, 33 {255, 128, 192}, 34 {255, 0, 0}, 35 }, 36 bounds: []int{0, 64 * 1024, 512 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024}, 37 }, 38 "rainbow": Theme{ 39 colorgroup: [][]int{ 40 {255, 255, 255}, 41 {102, 255, 102}, 42 {255, 255, 102}, 43 {255, 178, 102}, 44 {255, 102, 102}, 45 {226, 140, 255}, 46 {102, 204, 255}, 47 {102, 102, 255}, 48 }, 49 bounds: []int{0, 64 * 1024, 512 * 1024, 4 * 1024 * 1024, 16 * 1024 * 1024, 24 * 1024 * 1024, 32 * 1024 * 1024, 40 * 1024 * 1024}, 50 }, 51 } 52 53 i18n = map[string]string{ 54 "cn": `{ 55 "Title": "Lite SpeedTest 结果表", 56 "CreateAt": "测试时间", 57 "Traffic": "总流量: %s. 总时间: %s, 可用节点: [%s]" 58 }`, 59 "en": `{ 60 "Title": "Lite SpeedTest Result Table", 61 "CreateAt": "Create At", 62 "Traffic": "Traffic used: %s. Time used: %s, Working Nodes: [%s]" 63 }`, 64 } 65 ) 66 67 type Node struct { 68 Id int `json:"id"` 69 Group string `en:"Group" cn:"群组名" json:"group"` 70 Remarks string `en:"Remarks" cn:"备注" json:"remarks"` 71 Protocol string `en:"Protocol" cn:"协议" json:"protocol"` 72 Ping string `en:"Ping" cn:"Ping" json:"ping"` 73 AvgSpeed int64 `en:"AvgSpeed" cn:"平均速度" json:"avg_speed"` 74 MaxSpeed int64 `en:"MaxSpeed" cn:"最大速度" json:"max_speed"` 75 IsOk bool `json:"isok"` 76 Traffic int64 `json:"traffic"` 77 Link string `json:"link,omitempty"` // api only 78 } 79 80 func getNodeHeaders(language string) ([]string, map[string]string) { 81 kvs := map[string]string{} 82 var keys []string 83 t := reflect.TypeOf(Node{}) 84 for i := 0; i < t.NumField(); i++ { 85 f := t.Field(i) 86 if v, ok := f.Tag.Lookup(language); ok { 87 kvs[f.Name] = v 88 keys = append(keys, f.Name) 89 } 90 } 91 return keys, kvs 92 } 93 94 type Nodes []Node 95 96 func (nodes Nodes) Sort(sortMethod string) { 97 sort.Slice(nodes[:], func(i, j int) bool { 98 switch sortMethod { 99 case "speed": 100 return nodes[i].MaxSpeed < nodes[j].MaxSpeed 101 case "rspeed": 102 return nodes[i].MaxSpeed > nodes[j].MaxSpeed 103 case "ping": 104 return nodes[i].Ping < nodes[j].Ping 105 case "rping": 106 return nodes[i].Ping > nodes[j].Ping 107 default: 108 return true 109 } 110 }) 111 } 112 113 func CSV2Nodes(path string) (Nodes, error) { 114 recordFile, err := os.Open(path) 115 if err != nil { 116 return nil, err 117 } 118 defer recordFile.Close() 119 reader := csv.NewReader(recordFile) 120 records, err := reader.ReadAll() 121 if err != nil { 122 return nil, err 123 } 124 nodes := make(Nodes, len(records)) 125 for i, v := range records { 126 if len(v) < 6 { 127 continue 128 } 129 avg, err := strconv.Atoi(v[4]) 130 if err != nil { 131 continue 132 } 133 max, err := strconv.Atoi(v[5]) 134 if err != nil { 135 continue 136 } 137 nodes[i] = Node{ 138 Group: v[0], 139 Remarks: v[1], 140 Protocol: v[2], 141 Ping: v[3], 142 AvgSpeed: int64(avg), 143 MaxSpeed: int64(max), 144 } 145 } 146 return nodes, nil 147 } 148 149 type TableOptions struct { 150 horizontalpadding float64 // left + right 151 verticalpadding float64 // up + down 152 tableTopPadding float64 // padding for table 153 lineWidth float64 154 fontHeight float64 155 fontSize int 156 smallFontRatio float64 157 fontPath string 158 language string 159 theme Theme 160 timezone string 161 fontBytes []byte 162 } 163 164 func NewTableOptions(horizontalpadding float64, verticalpadding float64, tableTopPadding float64, 165 lineWidth float64, fontSize int, smallFontRatio float64, fontPath string, 166 language string, t string, timezone string, fontBytes []byte) TableOptions { 167 theme, ok := themes[t] 168 if !ok { 169 theme = themes["rainbow"] 170 } 171 return TableOptions{ 172 horizontalpadding: horizontalpadding, 173 verticalpadding: verticalpadding, 174 tableTopPadding: tableTopPadding, 175 lineWidth: lineWidth, 176 fontSize: fontSize, 177 smallFontRatio: smallFontRatio, 178 fontPath: fontPath, 179 language: language, 180 theme: theme, 181 timezone: timezone, 182 fontBytes: fontBytes, 183 } 184 } 185 186 type CellWidths struct { 187 Group float64 188 Remarks float64 189 Protocol float64 190 Ping float64 191 AvgSpeed float64 192 MaxSpeed float64 193 } 194 195 func (c CellWidths) toMap() map[string]float64 { 196 data, _ := json.Marshal(&c) 197 m := map[string]float64{} 198 // ignore error 199 json.Unmarshal(data, &m) 200 return m 201 } 202 203 type I18N struct { 204 CreateAt string 205 Title string 206 Traffic string 207 } 208 209 func NewI18N(data string) (*I18N, error) { 210 i18n := &I18N{} 211 err := json.Unmarshal([]byte(data), i18n) 212 if err != nil { 213 return nil, err 214 } 215 return i18n, nil 216 } 217 218 type Table struct { 219 width int 220 height int 221 *Context 222 nodes Nodes 223 options TableOptions 224 cellWidths *CellWidths 225 i18n *I18N 226 } 227 228 func NewTable(width int, height int, options TableOptions) Table { 229 dc := NewContext(width, height) 230 return Table{ 231 width: width, 232 height: height, 233 Context: dc, 234 options: options, 235 } 236 } 237 238 func DefaultTable(nodes Nodes, fontPath string) (*Table, error) { 239 options := NewTableOptions(40, 30, 0.5, 0.5, 24, 0.5, fontPath, "en", "rainbow", "Asia/Shanghai", nil) 240 return NewTableWithOption(nodes, &options) 241 } 242 243 // TODO: load font by name 244 func NewTableWithOption(nodes Nodes, options *TableOptions) (*Table, error) { 245 fontSize := options.fontSize 246 fontPath := options.fontPath 247 fontface, err := LoadFontFaceByBytes(options.fontBytes, fontPath, float64(fontSize)) 248 if err != nil { 249 return nil, err 250 } 251 widths := calcWidth(fontface, nodes) 252 fontHeight := calcHeight(fontface) 253 options.fontHeight = fontHeight 254 horizontalpadding := options.horizontalpadding 255 tableWidth := widths.Group + horizontalpadding + widths.Remarks + horizontalpadding + widths.Protocol + horizontalpadding + widths.Ping + horizontalpadding + widths.AvgSpeed + horizontalpadding + widths.MaxSpeed + horizontalpadding + options.lineWidth*2 256 tableHeight := (fontHeight+options.verticalpadding)*float64((len(nodes)+4)) + options.tableTopPadding*2 + options.fontHeight*options.smallFontRatio 257 table := NewTable(int(tableWidth), int(tableHeight), *options) 258 table.nodes = nodes 259 table.cellWidths = widths 260 result, err := NewI18N(i18n[options.language]) 261 if err != nil { 262 return nil, err 263 } 264 table.i18n = result 265 table.SetFontFace(fontface) 266 return &table, nil 267 } 268 269 func (t *Table) drawHorizonLines() { 270 y := t.options.tableTopPadding 271 for i := 0; i <= len(t.nodes)+4; i++ { 272 t.drawHorizonLine(y) 273 y += t.options.fontHeight + t.options.verticalpadding 274 } 275 } 276 277 func (t *Table) drawHorizonLine(y float64) { 278 t.DrawLine(0, y, float64(t.width), y) 279 t.SetLineWidth(t.options.lineWidth) 280 t.Stroke() 281 } 282 283 func (t *Table) drawVerticalLines() { 284 padding := t.options.horizontalpadding 285 var x float64 286 t.drawFullVerticalLine(t.options.lineWidth) 287 ks, _ := getNodeHeaders(t.options.language) 288 cellWidths := t.cellWidths.toMap() 289 for i := 1; i < len(cellWidths); i++ { 290 k := ks[i-1] 291 x += cellWidths[k] + padding 292 t.drawVerticalLine(x) 293 } 294 x += cellWidths[ks[len(cellWidths)-1]] + padding 295 t.drawFullVerticalLine(x) 296 } 297 298 func (t *Table) drawVerticalLine(x float64) { 299 height := (t.options.fontHeight+t.options.verticalpadding)*float64((len(t.nodes)+2)) + t.options.tableTopPadding 300 y := t.options.tableTopPadding + t.options.fontHeight + t.options.verticalpadding 301 t.DrawLine(x, y, x, height) 302 t.SetLineWidth(t.options.lineWidth) 303 t.Stroke() 304 } 305 306 func (t *Table) drawFullVerticalLine(x float64) { 307 height := (t.options.fontHeight+t.options.verticalpadding)*float64((len(t.nodes)+4)) + t.options.tableTopPadding 308 y := t.options.tableTopPadding 309 t.DrawLine(x, y, x, height) 310 t.SetLineWidth(t.options.lineWidth) 311 t.Stroke() 312 } 313 314 func (t *Table) drawTitle() { 315 // horizontalpadding := t.options.horizontalpadding 316 title := t.i18n.Title 317 var x float64 = float64(t.width)/2 - getWidth(t.fontFace, title)/2 318 var y float64 = t.options.fontHeight/2 + t.options.verticalpadding/2 + t.options.tableTopPadding 319 t.centerString(title, x, y) 320 } 321 322 func (t *Table) drawHeader() { 323 horizontalpadding := t.options.horizontalpadding 324 var x float64 = horizontalpadding / 2 325 var y float64 = t.options.fontHeight/2 + t.options.verticalpadding/2 + t.options.tableTopPadding + t.options.fontHeight + t.options.verticalpadding 326 cellWidths := t.cellWidths.toMap() 327 ks, kvs := getNodeHeaders(t.options.language) 328 for _, k := range ks { 329 adjust := cellWidths[k]/2 - getWidth(t.fontFace, kvs[k])/2 330 t.centerString(kvs[k], x+adjust, y) 331 x += cellWidths[k] + horizontalpadding 332 } 333 } 334 335 func (t *Table) drawTraffic(traffic string) { 336 // horizontalpadding := t.options.horizontalpadding 337 var x float64 = t.options.horizontalpadding / 2 338 var y float64 = (t.options.fontHeight+t.options.verticalpadding)*float64((len(t.nodes)+2)) + t.options.tableTopPadding + t.fontHeight/2 + t.options.verticalpadding/2 339 t.centerString(traffic, x, y) 340 } 341 342 func (t *Table) FormatTraffic(traffic string, time string, workingNode string) string { 343 return fmt.Sprintf(t.i18n.Traffic, traffic, time, workingNode) 344 } 345 346 func (t *Table) drawGeneratedAt() { 347 // horizontalpadding := t.options.horizontalpadding 348 msg := fmt.Sprintf("%s %s", t.i18n.CreateAt, time.Now().Format(time.RFC3339)) 349 // https://github.com/golang/go/issues/20455 350 if runtime.GOOS == "android" { 351 loc, _ := time.LoadLocation(t.options.timezone) 352 now := time.Now() 353 msg = fmt.Sprintf("%s %s", t.i18n.CreateAt, now.In(loc).Format(time.RFC3339)) 354 } 355 var x float64 = t.options.horizontalpadding / 2 356 var y float64 = (t.options.fontHeight+t.options.verticalpadding)*float64((len(t.nodes)+3)) + t.options.tableTopPadding + t.fontHeight/2 + t.options.verticalpadding/2 357 t.centerString(msg, x, y) 358 } 359 360 func (t *Table) drawPoweredBy() { 361 fontSize := int(float64(t.options.fontSize) * t.options.smallFontRatio) 362 fontface, err := LoadFontFaceByBytes(t.options.fontBytes, t.options.fontPath, float64(fontSize)) 363 if err != nil { 364 return 365 } 366 t.SetFontFace(fontface) 367 msg := constant.Version + " powered by https://github.com/xxf098" 368 var x float64 = float64(t.width) - getWidth(fontface, msg) - t.options.lineWidth 369 var y float64 = (t.options.fontHeight+t.options.verticalpadding)*float64((len(t.nodes)+4)) + t.options.fontHeight*t.options.smallFontRatio 370 t.DrawString(msg, x, y) 371 } 372 373 func (t *Table) centerString(s string, x, y float64) { 374 t.DrawStringAnchored(s, x, y, 0, 0.4) 375 } 376 377 func (t *Table) drawNodes() { 378 horizontalpadding := t.options.horizontalpadding 379 var x float64 = horizontalpadding / 2 380 var y float64 = t.options.fontHeight/2 + t.options.verticalpadding/2 + t.options.tableTopPadding + (t.options.fontHeight+t.options.verticalpadding)*2 381 for _, v := range t.nodes { 382 t.centerString(v.Group, x, y) 383 x += t.cellWidths.Group + horizontalpadding 384 t.centerString(v.Remarks, x, y) 385 x += t.cellWidths.Remarks + horizontalpadding 386 adjust := t.cellWidths.Protocol/2 - getWidth(t.fontFace, v.Protocol)/2 387 t.centerString(v.Protocol, x+adjust, y) 388 x += t.cellWidths.Protocol + horizontalpadding 389 adjust = t.cellWidths.Ping/2 - getWidth(t.fontFace, v.Ping)/2 390 t.centerString(v.Ping, x+adjust, y) 391 x += t.cellWidths.Ping + horizontalpadding 392 avgSpeed := download.ByteCountIECTrim(v.AvgSpeed) 393 adjust = t.cellWidths.AvgSpeed/2 - getWidth(t.fontFace, avgSpeed)/2 394 t.centerString(avgSpeed, x+adjust, y) 395 x += t.cellWidths.AvgSpeed + horizontalpadding 396 maxSpeed := download.ByteCountIECTrim(v.MaxSpeed) 397 adjust = t.cellWidths.MaxSpeed/2 - getWidth(t.fontFace, maxSpeed)/2 398 t.centerString(maxSpeed, x+adjust, y) 399 y += t.options.fontHeight + t.options.verticalpadding 400 x = horizontalpadding / 2 401 } 402 } 403 404 func (t *Table) drawSpeed() { 405 padding := t.options.horizontalpadding 406 var lineWidth float64 = t.options.lineWidth 407 var x1 float64 = t.cellWidths.Group + padding + t.cellWidths.Remarks + padding + t.cellWidths.Protocol + padding + t.cellWidths.Ping + padding + lineWidth 408 var x2 float64 = t.cellWidths.Group + padding + t.cellWidths.Remarks + padding + t.cellWidths.Protocol + padding + t.cellWidths.Ping + padding + t.cellWidths.AvgSpeed + padding + lineWidth 409 var y float64 = t.options.tableTopPadding + lineWidth + (t.options.fontHeight+t.options.verticalpadding)*2 410 var wAvg float64 = t.cellWidths.AvgSpeed + padding - lineWidth*2 411 var wMax float64 = t.cellWidths.MaxSpeed + padding - lineWidth*2 412 var h float64 = t.options.fontHeight + t.options.verticalpadding - 2*lineWidth 413 for i := 0; i < len(t.nodes); i++ { 414 t.DrawRectangle(x1, y, wAvg, h) 415 r, g, b := getSpeedColor(t.nodes[i].AvgSpeed, t.options.theme) 416 t.SetRGB255(r, g, b) 417 t.Fill() 418 t.DrawRectangle(x2, y, wMax, h) 419 r, g, b = getSpeedColor(t.nodes[i].MaxSpeed, t.options.theme) 420 t.SetRGB255(r, g, b) 421 t.Fill() 422 y = y + t.options.fontHeight + t.options.verticalpadding 423 } 424 t.SetRGB255(0, 0, 0) 425 } 426 427 func (t *Table) Draw(path string, traffic string) { 428 t.SetRGB255(255, 255, 255) 429 t.Clear() 430 t.SetRGB255(0, 0, 0) 431 t.drawHorizonLines() 432 t.drawVerticalLines() 433 t.drawSpeed() 434 t.drawTitle() 435 t.drawHeader() 436 t.drawNodes() 437 t.drawTraffic(traffic) 438 t.drawGeneratedAt() 439 t.drawPoweredBy() 440 t.SavePNG(path) 441 } 442 443 func (t *Table) Encode(traffic string) ([]byte, error) { 444 t.SetRGB255(255, 255, 255) 445 t.Clear() 446 t.SetRGB255(0, 0, 0) 447 t.drawHorizonLines() 448 t.drawVerticalLines() 449 t.drawSpeed() 450 t.drawTitle() 451 t.drawHeader() 452 t.drawNodes() 453 t.drawTraffic(traffic) 454 t.drawGeneratedAt() 455 t.drawPoweredBy() 456 var buf bytes.Buffer 457 err := t.EncodePNG(&buf) 458 if err != nil { 459 return nil, err 460 } 461 return buf.Bytes(), nil 462 } 463 464 func (t *Table) EncodeB64(traffic string) (string, error) { 465 bytes, err := t.Encode(traffic) 466 if err != nil { 467 return "", err 468 } 469 return "data:image/png;base64," + base64.StdEncoding.EncodeToString(bytes), nil 470 } 471 472 func getSpeedColor(speed int64, theme Theme) (int, int, int) { 473 bounds := theme.bounds 474 colorgroup := theme.colorgroup 475 for i := 0; i < len(bounds)-1; i++ { 476 if speed >= int64(bounds[i]) && speed <= int64(bounds[i+1]) { 477 level := float64(speed-int64(bounds[i])) / float64(bounds[i+1]-bounds[i]) 478 return getColor(colorgroup[i], colorgroup[i+1], level) 479 } 480 } 481 l := len(colorgroup) 482 return colorgroup[l-1][0], colorgroup[l-1][1], colorgroup[l-1][2] 483 } 484 485 func getColor(lc []int, rc []int, level float64) (int, int, int) { 486 r := float64(lc[0])*(1-level) + float64(rc[0])*level 487 g := float64(lc[1])*(1-level) + float64(rc[1])*level 488 b := float64(lc[2])*(1-level) + float64(rc[2])*level 489 return int(r), int(g), int(b) 490 } 491 492 func calcWidth(fontface font.Face, nodes Nodes) *CellWidths { 493 cellWidths := &CellWidths{} 494 if len(nodes) < 1 { 495 return cellWidths 496 } 497 cellWidths.Group = getWidth(fontface, nodes[0].Group) 498 cellWidths.Protocol = getWidth(fontface, "Protocol") 499 500 for _, v := range nodes { 501 width := getWidth(fontface, v.Ping) 502 if cellWidths.Ping < width { 503 cellWidths.Ping = width 504 } 505 width = getWidth(fontface, download.ByteCountIECTrim(v.AvgSpeed)) 506 if cellWidths.AvgSpeed < width { 507 cellWidths.AvgSpeed = width 508 } 509 width = getWidth(fontface, download.ByteCountIECTrim(v.MaxSpeed)) 510 if cellWidths.MaxSpeed < width { 511 cellWidths.MaxSpeed = width 512 } 513 width = getWidth(fontface, v.Remarks) 514 if cellWidths.Remarks < width { 515 cellWidths.Remarks = width 516 } 517 } 518 if cellWidths.Group < getWidth(fontface, "Group") { 519 cellWidths.Group = getWidth(fontface, "Group") 520 } 521 if cellWidths.Remarks < getWidth(fontface, "Remarks") { 522 cellWidths.Remarks = getWidth(fontface, "Remarks") 523 } 524 if cellWidths.Ping < getWidth(fontface, "Ping") { 525 cellWidths.Ping = getWidth(fontface, "Ping") 526 } 527 if cellWidths.AvgSpeed < getWidth(fontface, "AvgSpeed") { 528 cellWidths.AvgSpeed = getWidth(fontface, "AvgSpeed") 529 } 530 if cellWidths.MaxSpeed < getWidth(fontface, "MaxSpeed") { 531 cellWidths.MaxSpeed = getWidth(fontface, "MaxSpeed") 532 } 533 534 return cellWidths 535 } 536 537 func calcHeight(fontface font.Face) float64 { 538 return float64(fontface.Metrics().Height) / 64 539 } 540 541 func getWidth(fontface font.Face, s string) float64 { 542 a := font.MeasureString(fontface, s) 543 return float64(a >> 6) 544 }