...

Source file src/github.com/rivo/tview/image.go

Documentation: github.com/rivo/tview

     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 besed 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  }
   765  

View as plain text