image.go (25230B)
1 package tview 2 3 import ( 4 "image" 5 "math" 6 7 "github.com/gdamore/tcell/v2" 8 ) 9 10 // Types of dithering applied to images. 11 const ( 12 DitheringNone = iota // No dithering. 13 DitheringFloydSteinberg // Floyd-Steinberg dithering (the default). 14 ) 15 16 // The number of colors supported by true color terminals (R*G*B = 256*256*256). 17 const TrueColor = 16777216 18 19 // This map describes what each block element looks like. A 1 bit represents a 20 // pixel that is drawn, a 0 bit represents a pixel that is not drawn. The least 21 // significant bit is the top left pixel, the most significant bit is the bottom 22 // right pixel, moving row by row from left to right, top to bottom. 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 // pixel represents a character on screen used to draw part of an image. 46 type pixel struct { 47 style tcell.Style 48 element rune // The block element. 49 } 50 51 // Image implements a widget that displays one image. The original image 52 // (specified with [Image.SetImage]) is resized according to the specified size 53 // (see [Image.SetSize]), using the specified number of colors (see 54 // [Image.SetColors]), while applying dithering if necessary (see 55 // [Image.SetDithering]). 56 // 57 // Images are approximated by graphical characters in the terminal. The 58 // resolution is therefore limited by the number and type of characters that can 59 // be drawn in the terminal and the colors available in the terminal. The 60 // quality of the final image also depends on the terminal's font and spacing 61 // settings, none of which are under the control of this package. Results may 62 // vary. 63 type Image struct { 64 *Box 65 66 // The image to be displayed. If nil, the widget will be empty. 67 image image.Image 68 69 // The size of the image. If a value is 0, the corresponding size is chosen 70 // automatically based on the other size while preserving the image's aspect 71 // ratio. If both are 0, the image uses as much space as possible. A 72 // negative value represents a percentage, e.g. -50 means 50% of the 73 // available space. 74 width, height int 75 76 // The number of colors to use. If 0, the number of colors is chosen based 77 // on the terminal's capabilities. 78 colors int 79 80 // The dithering algorithm to use, one of the constants starting with 81 // "ImageDithering". 82 dithering int 83 84 // The width of a terminal's cell divided by its height. 85 aspectRatio float64 86 87 // Horizontal and vertical alignment, one of the "Align" constants. 88 alignHorizontal, alignVertical int 89 90 // The text to be displayed before the image. 91 label string 92 93 // The label style. 94 labelStyle tcell.Style 95 96 // The screen width of the label area. A value of 0 means use the width of 97 // the label text. 98 labelWidth int 99 100 // The actual image size (in cells) when it was drawn the last time. 101 lastWidth, lastHeight int 102 103 // The actual image (in cells) when it was drawn the last time. The size of 104 // this slice is lastWidth * lastHeight, indexed by y*lastWidth + x. 105 pixels []pixel 106 107 // A callback function set by the Form class and called when the user leaves 108 // this form item. 109 finished func(tcell.Key) 110 } 111 112 // NewImage returns a new image widget with an empty image (use [Image.SetImage] 113 // to specify the image to be displayed). The image will use the widget's entire 114 // available space. The dithering algorithm is set to Floyd-Steinberg dithering. 115 // The terminal's cell aspect ratio defaults to 0.5. 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 // SetImage sets the image to be displayed. If nil, the widget will be empty. 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 // SetSize sets the size of the image. Positive values refer to cells in the 134 // terminal. Negative values refer to a percentage of the available space (e.g. 135 // -50 means 50%). A value of 0 means that the corresponding size is chosen 136 // automatically based on the other size while preserving the image's aspect 137 // ratio. If both are 0, the image uses as much space as possible while still 138 // preserving the aspect ratio. 139 func (i *Image) SetSize(rows, columns int) *Image { 140 i.width = columns 141 i.height = rows 142 return i 143 } 144 145 // SetColors sets the number of colors to use. This should be the number of 146 // colors supported by the terminal. If 0, the number of colors is chosen based 147 // on the TERM environment variable (which may or may not be reliable). 148 // 149 // Only the values 0, 2, 8, 256, and 16777216 ([TrueColor]) are supported. Other 150 // values will be rounded up to the next supported value, to a maximum of 151 // 16777216. 152 // 153 // The effect of using more colors than supported by the terminal is undefined. 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 // GetColors returns the number of colors that will be used while drawing the 161 // image. This is one of the values listed in [Image.SetColors], except 0 which 162 // will be replaced by the actual number of colors used. 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 // SetDithering sets the dithering algorithm to use, one of the constants 178 // starting with "Dithering", for example [DitheringFloydSteinberg] (the 179 // default). Dithering is not applied when rendering in true-color. 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 // SetAspectRatio sets the width of a terminal's cell divided by its height. 187 // You may change the default of 0.5 if your terminal / font has a different 188 // aspect ratio. This is used to calculate the size of the image if the 189 // specified width or height is 0. The function will panic if the aspect ratio 190 // is 0 or less. 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 // SetAlign sets the vertical and horizontal alignment of the image within the 201 // widget's space. The possible values are [AlignTop], [AlignCenter], and 202 // [AlignBottom] for vertical alignment and [AlignLeft], [AlignCenter], and 203 // [AlignRight] for horizontal alignment. The default is [AlignCenter] for both 204 // (or [AlignTop] and [AlignLeft] if the image is part of a [Form]). 205 func (i *Image) SetAlign(vertical, horizontal int) *Image { 206 i.alignHorizontal = horizontal 207 i.alignVertical = vertical 208 return i 209 } 210 211 // SetLabel sets the text to be displayed before the image. 212 func (i *Image) SetLabel(label string) *Image { 213 i.label = label 214 return i 215 } 216 217 // GetLabel returns the text to be displayed before the image. 218 func (i *Image) GetLabel() string { 219 return i.label 220 } 221 222 // SetLabelWidth sets the screen width of the label. A value of 0 will cause the 223 // primitive to use the width of the label string. 224 func (i *Image) SetLabelWidth(width int) *Image { 225 i.labelWidth = width 226 return i 227 } 228 229 // GetFieldWidth returns this primitive's field width. This is the image's width 230 // or, if the width is 0 or less, the proportional width of the image based on 231 // its height as returned by [Image.GetFieldHeight]. If there is no image, 0 is 232 // returned. 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 // GetFieldHeight returns this primitive's field height. This is the image's 246 // height or 8 if the height is 0 or less. 247 func (i *Image) GetFieldHeight() int { 248 if i.height <= 0 { 249 return 8 250 } 251 return i.height 252 } 253 254 // SetDisabled sets whether or not the item is disabled / read-only. 255 func (i *Image) SetDisabled(disabled bool) FormItem { 256 return i // Images are always read-only. 257 } 258 259 // SetFormAttributes sets attributes shared by all form items. 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 // SetLabelStyle sets the style of the label. 269 func (i *Image) SetLabelStyle(style tcell.Style) *Image { 270 i.labelStyle = style 271 return i 272 } 273 274 // GetLabelStyle returns the style of the label. 275 func (i *Image) GetLabelStyle() tcell.Style { 276 return i.labelStyle 277 } 278 279 // SetFinishedFunc sets a callback invoked when the user leaves this form item. 280 func (i *Image) SetFinishedFunc(handler func(key tcell.Key)) FormItem { 281 i.finished = handler 282 return i 283 } 284 285 // Focus is called when this primitive receives focus. 286 func (i *Image) Focus(delegate func(p Primitive)) { 287 // If we're part of a form, there's nothing the user can do here so we're 288 // finished. 289 if i.finished != nil { 290 i.finished(-1) 291 return 292 } 293 294 i.Box.Focus(delegate) 295 } 296 297 // render re-populates the [Image.pixels] slice based on the current settings, 298 // if [Image.lastWidth] and [Image.lastHeight] don't match the current image's 299 // size. It also sets the new image size in these two variables. 300 func (i *Image) render() { 301 // If there is no image, there are no pixels. 302 if i.image == nil { 303 i.pixels = nil 304 return 305 } 306 307 // Calculate the new (terminal-space) image size. 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 // Use all available space. 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 // Turn percentages into absolute values. 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 // Adjust the width. 342 width = imageWidth * height / imageHeight 343 } else if height == 0 { 344 // Adjust the height. 345 height = imageHeight * width / imageWidth 346 } 347 } 348 if width <= 0 || height <= 0 { 349 i.pixels = nil 350 return 351 } 352 353 // If nothing has changed, we're done. 354 if i.lastWidth == width && i.lastHeight == height { 355 return 356 } 357 i.lastWidth, i.lastHeight = width, height // This could still be larger than the available space but that's ok for now. 358 359 // Generate the initial pixels by resizing the image (8x8 per cell). 360 pixels := i.resize() 361 362 // Turn them into block elements with background/foreground colors. 363 i.stamp(pixels) 364 } 365 366 // resize resizes the image to the current size and returns the result as a 367 // slice of pixels. It is assumed that [Image.lastWidth] (w) and 368 // [Image.lastHeight] (h) are positive, non-zero values, and the slice has a 369 // size of 64*w*h, with each pixel being represented by 3 float64 values in the 370 // range of 0-1. The factor of 64 is due to the fact that we calculate 8x8 371 // pixels per cell. 372 func (i *Image) resize() [][3]float64 { 373 // Because most of the time, we will be downsizing the image, we don't even 374 // attempt to do any fancy interpolation. For each target pixel, we 375 // calculate a weighted average of the source pixels using their coverage 376 // area. 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 // Iterate over all target pixels. Outer loop is Y. 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 // Inner loop is X. 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 // Add a weighted contribution to the target pixel. 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 // Normalize the pixels. 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 // stamp takes the pixels generated by [Image.resize] and populates the 440 // [Image.pixels] slice accordingly. 441 func (i *Image) stamp(resized [][3]float64) { 442 // For each 8x8 pixel block, we find the best block element to represent it, 443 // given the available colors. 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 // Calculate an error for each potential block element + color. Keep 449 // the one with the lowest error. 450 451 // Note that the values in "resize" may lie outside [0, 1] due to 452 // the error distribution during dithering. 453 454 minMSE := math.MaxFloat64 // Mean squared error. 455 var final [64][3]float64 // The final pixel values. 456 for element, bits := range blockElements { 457 // Calculate the average color for the pixels covered by the set 458 // bits and unset bits. 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 // Quantize to the nearest acceptable color. 497 for _, color := range []*[3]float64{&fg, &bg} { 498 if colors == 2 { 499 // Monochrome. The following weights correspond better 500 // to human perception than the arithmetic mean. 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 // Colors vary wildly for each terminal. Expect 512 // suboptimal results. 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 // Calculate the error (and the final pixel values). 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 // Do we have a better match? 550 if mse < minMSE { 551 // Yes. Save it. 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 // Check if there is a shade block which results in a smaller error. 563 564 // What's the overall average color? 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 // Quantize and choose shade element. 583 element := BlockFullBlock 584 var fg, bg tcell.Color 585 shades := []rune{' ', BlockLightShade, BlockMediumShade, BlockDarkShade, BlockFullBlock} 586 if colors == 2 { 587 // Monochrome. 588 gray := 0.299*avg[0] + 0.587*avg[1] + 0.114*avg[2] // See above for details. 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 // True color. 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 // 8 or 256 colors. 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++ { // Find the closest channel value. 624 best := math.Abs(avg[ch] - (lo[ch] + (hi[ch]-lo[ch])*shade)) // Start shade from lo to hi. 625 if value := math.Abs(avg[ch] - (hi[ch] - (hi[ch]-lo[ch])*shade)); value < best { 626 best = value // Swap lo and hi. 627 lo[ch], hi[ch] = hi[ch], lo[ch] 628 } 629 if value := math.Abs(avg[ch] - lo[ch]); value < best { 630 best = value // Use lo. 631 hi[ch] = lo[ch] 632 } 633 if value := math.Abs(avg[ch] - hi[ch]); value < best { 634 lo[ch] = hi[ch] // Use hi. 635 } 636 avg[ch] = lo[ch] + (hi[ch]-lo[ch])*shade // Quantize. 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 // Calculate the error (and the final pixel values). 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 // Is this shade element better than the block element? 661 if mse < minMSE { 662 // Yes. Save it. 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 // Apply dithering. 670 if colors < TrueColor && i.dithering == DitheringFloydSteinberg { 671 // The dithering mask determines how the error is distributed. 672 // Each element has three values: dx, dy, and weight (in 16th). 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 // We dither the 8x8 block as a 2x2 block, transferring errors 681 // to its 2x2 neighbors. 682 for ch := 0; ch < 3; ch++ { 683 for y := 0; y < 2; y++ { 684 for x := 0; x < 2; x++ { 685 // What's the error for this 4x4 block? 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 // Distribute it to the 2x2 neighbors. 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 // Draw draws this primitive onto the screen. 714 func (i *Image) Draw(screen tcell.Screen) { 715 i.DrawForSubclass(screen, i) 716 717 // Regenerate image if necessary. 718 i.render() 719 720 // Draw label. 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 // Determine image placement. 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 // Draw the image. 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 }