1 package tview
2
3 import (
4 "image"
5 "math"
6
7 "github.com/gdamore/tcell/v2"
8 )
9
10
11 const (
12 DitheringNone = iota
13 DitheringFloydSteinberg
14 )
15
16
17 const TrueColor = 16777216
18
19
20
21
22
23 var blockElements = map[rune]uint64{
24 BlockLowerOneEighthBlock: 0b1111111100000000000000000000000000000000000000000000000000000000,
25 BlockLowerOneQuarterBlock: 0b1111111111111111000000000000000000000000000000000000000000000000,
26 BlockLowerThreeEighthsBlock: 0b1111111111111111111111110000000000000000000000000000000000000000,
27 BlockLowerHalfBlock: 0b1111111111111111111111111111111100000000000000000000000000000000,
28 BlockLowerFiveEighthsBlock: 0b1111111111111111111111111111111111111111000000000000000000000000,
29 BlockLowerThreeQuartersBlock: 0b1111111111111111111111111111111111111111111111110000000000000000,
30 BlockLowerSevenEighthsBlock: 0b1111111111111111111111111111111111111111111111111111111100000000,
31 BlockLeftSevenEighthsBlock: 0b0111111101111111011111110111111101111111011111110111111101111111,
32 BlockLeftThreeQuartersBlock: 0b0011111100111111001111110011111100111111001111110011111100111111,
33 BlockLeftFiveEighthsBlock: 0b0001111100011111000111110001111100011111000111110001111100011111,
34 BlockLeftHalfBlock: 0b0000111100001111000011110000111100001111000011110000111100001111,
35 BlockLeftThreeEighthsBlock: 0b0000011100000111000001110000011100000111000001110000011100000111,
36 BlockLeftOneQuarterBlock: 0b0000001100000011000000110000001100000011000000110000001100000011,
37 BlockLeftOneEighthBlock: 0b0000000100000001000000010000000100000001000000010000000100000001,
38 BlockQuadrantLowerLeft: 0b0000111100001111000011110000111100000000000000000000000000000000,
39 BlockQuadrantLowerRight: 0b1111000011110000111100001111000000000000000000000000000000000000,
40 BlockQuadrantUpperLeft: 0b0000000000000000000000000000000000001111000011110000111100001111,
41 BlockQuadrantUpperRight: 0b0000000000000000000000000000000011110000111100001111000011110000,
42 BlockQuadrantUpperLeftAndLowerRight: 0b1111000011110000111100001111000000001111000011110000111100001111,
43 }
44
45
46 type pixel struct {
47 style tcell.Style
48 element rune
49 }
50
51
52
53
54
55
56
57
58
59
60
61
62
63 type Image struct {
64 *Box
65
66
67 image image.Image
68
69
70
71
72
73
74 width, height int
75
76
77
78 colors int
79
80
81
82 dithering int
83
84
85 aspectRatio float64
86
87
88 alignHorizontal, alignVertical int
89
90
91 label string
92
93
94 labelStyle tcell.Style
95
96
97
98 labelWidth int
99
100
101 lastWidth, lastHeight int
102
103
104
105 pixels []pixel
106
107
108
109 finished func(tcell.Key)
110 }
111
112
113
114
115
116 func NewImage() *Image {
117 return &Image{
118 Box: NewBox(),
119 dithering: DitheringFloydSteinberg,
120 aspectRatio: 0.5,
121 alignHorizontal: AlignCenter,
122 alignVertical: AlignCenter,
123 }
124 }
125
126
127 func (i *Image) SetImage(image image.Image) *Image {
128 i.image = image
129 i.lastWidth, i.lastHeight = 0, 0
130 return i
131 }
132
133
134
135
136
137
138
139 func (i *Image) SetSize(rows, columns int) *Image {
140 i.width = columns
141 i.height = rows
142 return i
143 }
144
145
146
147
148
149
150
151
152
153
154 func (i *Image) SetColors(colors int) *Image {
155 i.colors = colors
156 i.lastWidth, i.lastHeight = 0, 0
157 return i
158 }
159
160
161
162
163 func (i *Image) GetColors() int {
164 switch {
165 case i.colors == 0:
166 return availableColors
167 case i.colors <= 2:
168 return 2
169 case i.colors <= 8:
170 return 8
171 case i.colors <= 256:
172 return 256
173 }
174 return TrueColor
175 }
176
177
178
179
180 func (i *Image) SetDithering(dithering int) *Image {
181 i.dithering = dithering
182 i.lastWidth, i.lastHeight = 0, 0
183 return i
184 }
185
186
187
188
189
190
191 func (i *Image) SetAspectRatio(aspectRatio float64) *Image {
192 if aspectRatio <= 0 {
193 panic("aspect ratio must be greater than 0")
194 }
195 i.aspectRatio = aspectRatio
196 i.lastWidth, i.lastHeight = 0, 0
197 return i
198 }
199
200
201
202
203
204
205 func (i *Image) SetAlign(vertical, horizontal int) *Image {
206 i.alignHorizontal = horizontal
207 i.alignVertical = vertical
208 return i
209 }
210
211
212 func (i *Image) SetLabel(label string) *Image {
213 i.label = label
214 return i
215 }
216
217
218 func (i *Image) GetLabel() string {
219 return i.label
220 }
221
222
223
224 func (i *Image) SetLabelWidth(width int) *Image {
225 i.labelWidth = width
226 return i
227 }
228
229
230
231
232
233 func (i *Image) GetFieldWidth() int {
234 if i.width <= 0 {
235 if i.image == nil {
236 return 0
237 }
238 bounds := i.image.Bounds()
239 height := i.GetFieldHeight()
240 return bounds.Dx() * height / bounds.Dy()
241 }
242 return i.width
243 }
244
245
246
247 func (i *Image) GetFieldHeight() int {
248 if i.height <= 0 {
249 return 8
250 }
251 return i.height
252 }
253
254
255 func (i *Image) SetDisabled(disabled bool) FormItem {
256 return i
257 }
258
259
260 func (i *Image) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
261 i.labelWidth = labelWidth
262 i.backgroundColor = bgColor
263 i.SetLabelStyle(tcell.StyleDefault.Foreground(labelColor).Background(bgColor))
264 i.lastWidth, i.lastHeight = 0, 0
265 return i
266 }
267
268
269 func (i *Image) SetLabelStyle(style tcell.Style) *Image {
270 i.labelStyle = style
271 return i
272 }
273
274
275 func (i *Image) GetLabelStyle() tcell.Style {
276 return i.labelStyle
277 }
278
279
280 func (i *Image) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
281 i.finished = handler
282 return i
283 }
284
285
286 func (i *Image) Focus(delegate func(p Primitive)) {
287
288
289 if i.finished != nil {
290 i.finished(-1)
291 return
292 }
293
294 i.Box.Focus(delegate)
295 }
296
297
298
299
300 func (i *Image) render() {
301
302 if i.image == nil {
303 i.pixels = nil
304 return
305 }
306
307
308 bounds := i.image.Bounds()
309 imageWidth, imageHeight := bounds.Dx(), bounds.Dy()
310 if i.aspectRatio != 1.0 {
311 imageWidth = int(float64(imageWidth) / i.aspectRatio)
312 }
313 width, height := i.width, i.height
314 _, _, innerWidth, innerHeight := i.GetInnerRect()
315 if i.labelWidth > 0 {
316 innerWidth -= i.labelWidth
317 } else {
318 innerWidth -= TaggedStringWidth(i.label)
319 }
320 if innerWidth <= 0 {
321 i.pixels = nil
322 return
323 }
324 if width == 0 && height == 0 {
325
326 width, height = innerWidth, innerHeight
327 if adjustedWidth := imageWidth * height / imageHeight; adjustedWidth < width {
328 width = adjustedWidth
329 } else {
330 height = imageHeight * width / imageWidth
331 }
332 } else {
333
334 if width < 0 {
335 width = innerWidth * -width / 100
336 }
337 if height < 0 {
338 height = innerHeight * -height / 100
339 }
340 if width == 0 {
341
342 width = imageWidth * height / imageHeight
343 } else if height == 0 {
344
345 height = imageHeight * width / imageWidth
346 }
347 }
348 if width <= 0 || height <= 0 {
349 i.pixels = nil
350 return
351 }
352
353
354 if i.lastWidth == width && i.lastHeight == height {
355 return
356 }
357 i.lastWidth, i.lastHeight = width, height
358
359
360 pixels := i.resize()
361
362
363 i.stamp(pixels)
364 }
365
366
367
368
369
370
371
372 func (i *Image) resize() [][3]float64 {
373
374
375
376
377
378 bounds := i.image.Bounds()
379 srcWidth, srcHeight := bounds.Dx(), bounds.Dy()
380 tgtWidth, tgtHeight := i.lastWidth*8, i.lastHeight*8
381 coverageWidth, coverageHeight := float64(tgtWidth)/float64(srcWidth), float64(tgtHeight)/float64(srcHeight)
382 pixels := make([][3]float64, tgtWidth*tgtHeight)
383 weights := make([]float64, tgtWidth*tgtHeight)
384 for srcY := bounds.Min.Y; srcY < bounds.Max.Y; srcY++ {
385 for srcX := bounds.Min.X; srcX < bounds.Max.X; srcX++ {
386 r32, g32, b32, _ := i.image.At(srcX, srcY).RGBA()
387 r, g, b := float64(r32)/0xffff, float64(g32)/0xffff, float64(b32)/0xffff
388
389
390 startY := float64(srcY-bounds.Min.Y) * coverageHeight
391 endY := startY + coverageHeight
392 fromY, toY := int(startY), int(endY)
393 for tgtY := fromY; tgtY <= toY && tgtY < tgtHeight; tgtY++ {
394 coverageY := 1.0
395 if tgtY == fromY {
396 coverageY -= math.Mod(startY, 1.0)
397 }
398 if tgtY == toY {
399 coverageY -= 1.0 - math.Mod(endY, 1.0)
400 }
401
402
403 startX := float64(srcX-bounds.Min.X) * coverageWidth
404 endX := startX + coverageWidth
405 fromX, toX := int(startX), int(endX)
406 for tgtX := fromX; tgtX <= toX && tgtX < tgtWidth; tgtX++ {
407 coverageX := 1.0
408 if tgtX == fromX {
409 coverageX -= math.Mod(startX, 1.0)
410 }
411 if tgtX == toX {
412 coverageX -= 1.0 - math.Mod(endX, 1.0)
413 }
414
415
416 index := tgtY*tgtWidth + tgtX
417 coverage := coverageX * coverageY
418 pixels[index][0] += r * coverage
419 pixels[index][1] += g * coverage
420 pixels[index][2] += b * coverage
421 weights[index] += coverage
422 }
423 }
424 }
425 }
426
427
428 for index, weight := range weights {
429 if weight > 0 {
430 pixels[index][0] /= weight
431 pixels[index][1] /= weight
432 pixels[index][2] /= weight
433 }
434 }
435
436 return pixels
437 }
438
439
440
441 func (i *Image) stamp(resized [][3]float64) {
442
443
444 i.pixels = make([]pixel, i.lastWidth*i.lastHeight)
445 colors := i.GetColors()
446 for row := 0; row < i.lastHeight; row++ {
447 for col := 0; col < i.lastWidth; col++ {
448
449
450
451
452
453
454 minMSE := math.MaxFloat64
455 var final [64][3]float64
456 for element, bits := range blockElements {
457
458
459 var (
460 bg, fg [3]float64
461 setBits float64
462 bit uint64 = 1
463 )
464 for y := 0; y < 8; y++ {
465 for x := 0; x < 8; x++ {
466 index := (row*8+y)*i.lastWidth*8 + (col*8 + x)
467 if bits&bit != 0 {
468 fg[0] += resized[index][0]
469 fg[1] += resized[index][1]
470 fg[2] += resized[index][2]
471 setBits++
472 } else {
473 bg[0] += resized[index][0]
474 bg[1] += resized[index][1]
475 bg[2] += resized[index][2]
476 }
477 bit <<= 1
478 }
479 }
480 for ch := 0; ch < 3; ch++ {
481 fg[ch] /= setBits
482 if fg[ch] < 0 {
483 fg[ch] = 0
484 } else if fg[ch] > 1 {
485 fg[ch] = 1
486 }
487 bg[ch] /= 64 - setBits
488 if bg[ch] < 0 {
489 bg[ch] = 0
490 }
491 if bg[ch] > 1 {
492 bg[ch] = 1
493 }
494 }
495
496
497 for _, color := range []*[3]float64{&fg, &bg} {
498 if colors == 2 {
499
500
501 gray := 0.299*color[0] + 0.587*color[1] + 0.114*color[2]
502 if gray < 0.5 {
503 *color = [3]float64{0, 0, 0}
504 } else {
505 *color = [3]float64{1, 1, 1}
506 }
507 } else {
508 for index, ch := range color {
509 switch {
510 case colors == 8:
511
512
513 if ch < 0.5 {
514 color[index] = 0
515 } else {
516 color[index] = 1
517 }
518 case colors == 256:
519 color[index] = math.Round(ch*6) / 6
520 }
521 }
522 }
523 }
524
525
526 var (
527 mse float64
528 values [64][3]float64
529 valuesIndex int
530 )
531 bit = 1
532 for y := 0; y < 8; y++ {
533 for x := 0; x < 8; x++ {
534 if bits&bit != 0 {
535 values[valuesIndex] = fg
536 } else {
537 values[valuesIndex] = bg
538 }
539 index := (row*8+y)*i.lastWidth*8 + (col*8 + x)
540 for ch := 0; ch < 3; ch++ {
541 err := resized[index][ch] - values[valuesIndex][ch]
542 mse += err * err
543 }
544 bit <<= 1
545 valuesIndex++
546 }
547 }
548
549
550 if mse < minMSE {
551
552 minMSE = mse
553 final = values
554 index := row*i.lastWidth + col
555 i.pixels[index].element = element
556 i.pixels[index].style = tcell.StyleDefault.
557 Foreground(tcell.NewRGBColor(int32(math.Min(255, fg[0]*255)), int32(math.Min(255, fg[1]*255)), int32(math.Min(255, fg[2]*255)))).
558 Background(tcell.NewRGBColor(int32(math.Min(255, bg[0]*255)), int32(math.Min(255, bg[1]*255)), int32(math.Min(255, bg[2]*255))))
559 }
560 }
561
562
563
564
565 var avg [3]float64
566 for y := 0; y < 8; y++ {
567 for x := 0; x < 8; x++ {
568 index := (row*8+y)*i.lastWidth*8 + (col*8 + x)
569 for ch := 0; ch < 3; ch++ {
570 avg[ch] += resized[index][ch] / 64
571 }
572 }
573 }
574 for ch := 0; ch < 3; ch++ {
575 if avg[ch] < 0 {
576 avg[ch] = 0
577 } else if avg[ch] > 1 {
578 avg[ch] = 1
579 }
580 }
581
582
583 element := BlockFullBlock
584 var fg, bg tcell.Color
585 shades := []rune{' ', BlockLightShade, BlockMediumShade, BlockDarkShade, BlockFullBlock}
586 if colors == 2 {
587
588 gray := 0.299*avg[0] + 0.587*avg[1] + 0.114*avg[2]
589 shade := int(math.Round(gray * 4))
590 element = shades[shade]
591 for ch := 0; ch < 3; ch++ {
592 avg[ch] = float64(shade) / 4
593 }
594 bg = tcell.ColorBlack
595 fg = tcell.ColorWhite
596 } else if colors == TrueColor {
597
598 fg = tcell.NewRGBColor(int32(math.Min(255, avg[0]*255)), int32(math.Min(255, avg[1]*255)), int32(math.Min(255, avg[2]*255)))
599 bg = fg
600 } else {
601
602 steps := 1.0
603 if colors == 256 {
604 steps = 6.0
605 }
606 var (
607 lo, hi, pos [3]float64
608 shade float64
609 )
610 for ch := 0; ch < 3; ch++ {
611 lo[ch] = math.Floor(avg[ch]*steps) / steps
612 hi[ch] = math.Ceil(avg[ch]*steps) / steps
613 if r := hi[ch] - lo[ch]; r > 0 {
614 pos[ch] = (avg[ch] - lo[ch]) / r
615 if math.Abs(pos[ch]-0.5) < math.Abs(shade-0.5) {
616 shade = pos[ch]
617 }
618 }
619 }
620 shade = math.Round(shade * 4)
621 element = shades[int(shade)]
622 shade /= 4
623 for ch := 0; ch < 3; ch++ {
624 best := math.Abs(avg[ch] - (lo[ch] + (hi[ch]-lo[ch])*shade))
625 if value := math.Abs(avg[ch] - (hi[ch] - (hi[ch]-lo[ch])*shade)); value < best {
626 best = value
627 lo[ch], hi[ch] = hi[ch], lo[ch]
628 }
629 if value := math.Abs(avg[ch] - lo[ch]); value < best {
630 best = value
631 hi[ch] = lo[ch]
632 }
633 if value := math.Abs(avg[ch] - hi[ch]); value < best {
634 lo[ch] = hi[ch]
635 }
636 avg[ch] = lo[ch] + (hi[ch]-lo[ch])*shade
637 }
638 bg = tcell.NewRGBColor(int32(math.Min(255, lo[0]*255)), int32(math.Min(255, lo[1]*255)), int32(math.Min(255, lo[2]*255)))
639 fg = tcell.NewRGBColor(int32(math.Min(255, hi[0]*255)), int32(math.Min(255, hi[1]*255)), int32(math.Min(255, hi[2]*255)))
640 }
641
642
643 var (
644 mse float64
645 values [64][3]float64
646 valuesIndex int
647 )
648 for y := 0; y < 8; y++ {
649 for x := 0; x < 8; x++ {
650 index := (row*8+y)*i.lastWidth*8 + (col*8 + x)
651 for ch := 0; ch < 3; ch++ {
652 err := resized[index][ch] - avg[ch]
653 mse += err * err
654 }
655 values[valuesIndex] = avg
656 valuesIndex++
657 }
658 }
659
660
661 if mse < minMSE {
662
663 final = values
664 index := row*i.lastWidth + col
665 i.pixels[index].element = element
666 i.pixels[index].style = tcell.StyleDefault.Foreground(fg).Background(bg)
667 }
668
669
670 if colors < TrueColor && i.dithering == DitheringFloydSteinberg {
671
672
673 var mask = [4][3]int{
674 {1, 0, 7},
675 {-1, 1, 3},
676 {0, 1, 5},
677 {1, 1, 1},
678 }
679
680
681
682 for ch := 0; ch < 3; ch++ {
683 for y := 0; y < 2; y++ {
684 for x := 0; x < 2; x++ {
685
686 var err float64
687 for dy := 0; dy < 4; dy++ {
688 for dx := 0; dx < 4; dx++ {
689 err += (final[(y*4+dy)*8+(x*4+dx)][ch] - resized[(row*8+(y*4+dy))*i.lastWidth*8+(col*8+(x*4+dx))][ch]) / 16
690 }
691 }
692
693
694 for _, dist := range mask {
695 for dy := 0; dy < 4; dy++ {
696 for dx := 0; dx < 4; dx++ {
697 targetX, targetY := (x+dist[0])*4+dx, (y+dist[1])*4+dy
698 if targetX < 0 || col*8+targetX >= i.lastWidth*8 || targetY < 0 || row*8+targetY >= i.lastHeight*8 {
699 continue
700 }
701 resized[(row*8+targetY)*i.lastWidth*8+(col*8+targetX)][ch] -= err * float64(dist[2]) / 16
702 }
703 }
704 }
705 }
706 }
707 }
708 }
709 }
710 }
711 }
712
713
714 func (i *Image) Draw(screen tcell.Screen) {
715 i.DrawForSubclass(screen, i)
716
717
718 i.render()
719
720
721 viewX, viewY, viewWidth, viewHeight := i.GetInnerRect()
722 _, labelBg, _ := i.labelStyle.Decompose()
723 if i.labelWidth > 0 {
724 labelWidth := i.labelWidth
725 if labelWidth > viewWidth {
726 labelWidth = viewWidth
727 }
728 printWithStyle(screen, i.label, viewX, viewY, 0, labelWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
729 viewX += labelWidth
730 viewWidth -= labelWidth
731 } else {
732 _, drawnWidth, _, _ := printWithStyle(screen, i.label, viewX, viewY, 0, viewWidth, AlignLeft, i.labelStyle, labelBg == tcell.ColorDefault)
733 viewX += drawnWidth
734 viewWidth -= drawnWidth
735 }
736
737
738 x, y, width, height := viewX, viewY, i.lastWidth, i.lastHeight
739 if i.alignHorizontal == AlignCenter {
740 x += (viewWidth - width) / 2
741 } else if i.alignHorizontal == AlignRight {
742 x += viewWidth - width
743 }
744 if i.alignVertical == AlignCenter {
745 y += (viewHeight - height) / 2
746 } else if i.alignVertical == AlignBottom {
747 y += viewHeight - height
748 }
749
750
751 for row := 0; row < height; row++ {
752 if y+row < viewY || y+row >= viewY+viewHeight {
753 continue
754 }
755 for col := 0; col < width; col++ {
756 if x+col < viewX || x+col >= viewX+viewWidth {
757 continue
758 }
759
760 index := row*width + col
761 screen.SetContent(x+col, y+row, i.pixels[index].element, nil, i.pixels[index].style)
762 }
763 }
764 }
765
View as plain text