Skip to content

Conversation

raphamorim
Copy link
Member

@raphamorim raphamorim commented Feb 27, 2025

Convert images to strings using half blocks symbols

API

raw, err := loadImage("./../fixtures/graphics/JigokudaniMonkeyPark.png")
if err != nil {
	os.Exit(1)
}

// Create options
options := EncoderOptions{
	ColorMode:    3,
	Width:        80,
	Height:       0,
	Threshold:    uint8(128),
	DitherLevel:  0.0,
	UseFgBgOnly:  false,
	InvertColors: false,
	ScaleMode:    "default",
	Symbols:      "half",
}

result := Encode(raw, options)
fmt.Println(result)

Result:

Screenshot 2025-02-27 at 12 33 37

Other examples

Screenshot 2025-02-27 at 12 35 31


// EncoderOptions contains all configurable settings
type EncoderOptions struct {
ColorMode int // 0=none, 1=8colors, 2=256colors, 3=truecolor
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably don't need this since downsampling colors is handled by colorprofile

DitherLevel float64 // Dithering amount (0.0-1.0)
UseFgBgOnly bool // Use only foreground/background colors (no block symbols)
InvertColors bool // Invert colors
ScaleMode string // fit, stretch, center
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's define a type for this one, maybe use draw.Scaler https://pkg.go.dev/golang.org/x/image/draw#Scaler

@raphamorim
Copy link
Member Author

btw we use the term enconding but in halfblocks isn't reaaaallly encoding. I think makes more sense to rename to render term

@raphamorim
Copy link
Member Author

talked with @aymanbagabas and we think it makes more sense to live in lipgloss. Will be moving it to there and address the comments

}

// getPixelSafe returns the color at (x,y) or black if out of bounds
func (r *Renderer) getPixelSafe(img image.Image, x, y int) color.RGBA {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use color.Color instead of color.RGBA all over

func (r *Renderer) getPixelSafe(img image.Image, x, y int) color.RGBA {
bounds := img.Bounds()
if x < bounds.Min.X || x >= bounds.Max.X || y < bounds.Min.Y || y >= bounds.Max.Y {
return color.RGBA{0, 0, 0, 255}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be

Suggested change
return color.RGBA{0, 0, 0, 255}
return color.Black

Comment on lines +343 to +349
r8, g8, b8, a8 := img.At(x, y).RGBA()
return color.RGBA{
R: uint8(r8 >> 8),
G: uint8(g8 >> 8),
B: uint8(b8 >> 8),
A: uint8(a8 >> 8),
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
r8, g8, b8, a8 := img.At(x, y).RGBA()
return color.RGBA{
R: uint8(r8 >> 8),
G: uint8(g8 >> 8),
B: uint8(b8 >> 8),
A: uint8(a8 >> 8),
}
return img.At(x, y)

}

// scaleImage resizes an image to the specified dimensions
func (r *Renderer) scaleImage(img image.Image, width, height int) image.Image {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All scale functions should adhere to draw.Scaler

Comment on lines +313 to +333
var fgStr, bgStr string

switch r.Options.ColorMode {
case 1: // 8 colors
fgCode := nearestAnsi8Color(fg.R, fg.G, fg.B)
bgCode := nearestAnsi8Color(bg.R, bg.G, bg.B)
fgStr = fmt.Sprintf("\033[%dm", 30+fgCode)
bgStr = fmt.Sprintf("\033[%dm", 40+bgCode)

case 2: // 256 colors
fgCode := nearestAnsi256Color(fg.R, fg.G, fg.B)
bgCode := nearestAnsi256Color(bg.R, bg.G, bg.B)
fgStr = fmt.Sprintf("\033[38;5;%dm", fgCode)
bgStr = fmt.Sprintf("\033[48;5;%dm", bgCode)

case 3: // True color
fgStr = fmt.Sprintf("\033[38;2;%d;%d;%dm", fg.R, fg.G, fg.B)
bgStr = fmt.Sprintf("\033[48;2;%d;%d;%dm", bg.R, bg.G, bg.B)
}

return fgStr + bgStr + string(char) + "\033[0m"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use ansi.Style with color.Color instead. Downgrading colors can should be handled by colorprofile. We have types for 16-bit and indexed in ansi, specifically ansi.BasicColor, ansi.IndexedColor, any other color.Color type will be treated as a true color.

The only optimization I can think of is checking if the RGB value corresponds to a 256 terminal color and using that to encode the color saving some SGR bytes i.e. if the color is #5f0000 we can use the defined XTerm dark red (indexed 52) color instead of to save bytes \x1b[48;2;95;0;0m vs `\x1b[48;5;52m'.

Anyway, we should use ansi.Style{}.ForegroundColor(fg).BackgroundColor(bg).Styled(string(char)) instead

@raphamorim
Copy link
Member Author

Moving it to charmbracelet/lipgloss#482

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants