nt

A sensible note-taking program
git clone git://git.laack.co/nt.git
Log | Files | Refs | README

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 }