mirror of
https://github.com/YuzuZensai/TrollSSH.git
synced 2026-09-13 23:19:02 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7e831f373
|
+51
-2
@@ -1,10 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -169,7 +173,11 @@ func TestFrameToAscii(t *testing.T) {
|
|||||||
// Below threshold -> first ramp char; full brightness -> last.
|
// Below threshold -> first ramp char; full brightness -> last.
|
||||||
opts := asciiOptions{brightnessThreshold: 40, charset: "standard"}
|
opts := asciiOptions{brightnessThreshold: 40, charset: "standard"}
|
||||||
ramp := []rune(resolveCharset("standard"))
|
ramp := []rune(resolveCharset("standard"))
|
||||||
out := []rune(frameToAscii([]byte{0, 255}, ramp, opts))
|
img := &image.RGBA{
|
||||||
|
Pix: []byte{0, 0, 0, 255, 255, 255, 255, 255},
|
||||||
|
Stride: 8, Rect: image.Rect(0, 0, 2, 1),
|
||||||
|
}
|
||||||
|
out := []rune(frameToAscii(img, buildRampLUT(ramp, opts)))
|
||||||
if out[0] != ramp[0] {
|
if out[0] != ramp[0] {
|
||||||
t.Errorf("dark px = %q, want %q", out[0], ramp[0])
|
t.Errorf("dark px = %q, want %q", out[0], ramp[0])
|
||||||
}
|
}
|
||||||
@@ -181,12 +189,53 @@ func TestFrameToAscii(t *testing.T) {
|
|||||||
func TestFrameToAsciiInvert(t *testing.T) {
|
func TestFrameToAsciiInvert(t *testing.T) {
|
||||||
opts := asciiOptions{brightnessThreshold: 40, charset: "standard", invert: true}
|
opts := asciiOptions{brightnessThreshold: 40, charset: "standard", invert: true}
|
||||||
ramp := []rune(resolveCharset("standard"))
|
ramp := []rune(resolveCharset("standard"))
|
||||||
out := []rune(frameToAscii([]byte{255}, ramp, opts))
|
img := &image.RGBA{
|
||||||
|
Pix: []byte{255, 255, 255, 255},
|
||||||
|
Stride: 4, Rect: image.Rect(0, 0, 1, 1),
|
||||||
|
}
|
||||||
|
out := []rune(frameToAscii(img, buildRampLUT(ramp, opts)))
|
||||||
if out[0] != ramp[0] {
|
if out[0] != ramp[0] {
|
||||||
t.Errorf("inverted bright = %q, want %q", out[0], ramp[0])
|
t.Errorf("inverted bright = %q, want %q", out[0], ramp[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderConcurrentSameKey(t *testing.T) {
|
||||||
|
var jpegBuf bytes.Buffer
|
||||||
|
src := image.NewRGBA(image.Rect(0, 0, 16, 16))
|
||||||
|
for i := range src.Pix {
|
||||||
|
src.Pix[i] = byte(i * 7)
|
||||||
|
}
|
||||||
|
if err := jpeg.Encode(&jpegBuf, src, nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := newFrameRenderer(0, [][]byte{jpegBuf.Bytes()}, asciiOptions{
|
||||||
|
brightnessThreshold: 40,
|
||||||
|
charset: "standard",
|
||||||
|
}, newRenderCache(1<<20))
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
results := make([]string, 32)
|
||||||
|
for i := range results {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(i int) {
|
||||||
|
defer wg.Done()
|
||||||
|
ascii, err := r.render(0, 20, 10, false, colorTierTrueColor)
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results[i] = ascii
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
for i, got := range results {
|
||||||
|
if got != results[0] {
|
||||||
|
t.Fatalf("result %d differs from result 0", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderCacheEvictsByBytes(t *testing.T) {
|
func TestRenderCacheEvictsByBytes(t *testing.T) {
|
||||||
budget := 3 * entryCost("k", strings.Repeat("x", 1000))
|
budget := 3 * entryCost("k", strings.Repeat("x", 1000))
|
||||||
c := newRenderCache(budget)
|
c := newRenderCache(budget)
|
||||||
|
|||||||
+75
-40
@@ -94,23 +94,16 @@ func putOutBuf(b []byte) {
|
|||||||
outPool.Put(&b)
|
outPool.Put(&b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resizeFrame(frame, pix []byte, width, height int, keepAspectRatio bool, tier colorTier) (draw.Image, error) {
|
func resizeFrame(frame, pix []byte, width, height int, keepAspectRatio bool) (*image.RGBA, error) {
|
||||||
src, err := jpeg.Decode(bytes.NewReader(frame))
|
src, err := jpeg.Decode(bytes.NewReader(frame))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
rect := image.Rect(0, 0, width, height)
|
rect := image.Rect(0, 0, width, height)
|
||||||
var dst draw.Image
|
|
||||||
var bg color.Color
|
dst := &image.RGBA{Pix: pix[:4*width*height], Stride: 4 * width, Rect: rect}
|
||||||
if tier == colorTierNone {
|
|
||||||
dst = &image.Gray{Pix: pix[:width*height], Stride: width, Rect: rect}
|
|
||||||
bg = color.Gray{0}
|
|
||||||
} else {
|
|
||||||
dst = &image.NRGBA{Pix: pix[:4*width*height], Stride: 4 * width, Rect: rect}
|
|
||||||
bg = color.Black
|
|
||||||
}
|
|
||||||
if keepAspectRatio {
|
if keepAspectRatio {
|
||||||
draw.Draw(dst, dst.Bounds(), image.NewUniform(bg), image.Point{}, draw.Src)
|
draw.Draw(dst, dst.Bounds(), image.NewUniform(color.Black), image.Point{}, draw.Src)
|
||||||
sb := src.Bounds()
|
sb := src.Bounds()
|
||||||
sw, sh := sb.Dx(), sb.Dy()
|
sw, sh := sb.Dx(), sb.Dy()
|
||||||
scale := min(float64(width)/float64(sw), float64(height)/float64(sh))
|
scale := min(float64(width)/float64(sw), float64(height)/float64(sh))
|
||||||
@@ -131,6 +124,15 @@ type asciiOptions struct {
|
|||||||
invert bool
|
invert bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildRampLUT(ramp []rune, options asciiOptions) *[101][]byte {
|
||||||
|
var lut [101][]byte
|
||||||
|
for b := range lut {
|
||||||
|
index := rampIndex(b, options.brightnessThreshold, len(ramp), options.invert)
|
||||||
|
lut[b] = utf8.AppendRune(nil, ramp[index])
|
||||||
|
}
|
||||||
|
return &lut
|
||||||
|
}
|
||||||
|
|
||||||
func rampIndex(brightness, threshold, total int, invert bool) int {
|
func rampIndex(brightness, threshold, total int, invert bool) int {
|
||||||
var index int
|
var index int
|
||||||
if brightness < threshold {
|
if brightness < threshold {
|
||||||
@@ -147,13 +149,12 @@ func rampIndex(brightness, threshold, total int, invert bool) int {
|
|||||||
return index
|
return index
|
||||||
}
|
}
|
||||||
|
|
||||||
func frameToAscii(pixels []byte, ramp []rune, options asciiOptions) string {
|
func frameToAscii(img *image.RGBA, rampLUT *[101][]byte) string {
|
||||||
total := len(ramp)
|
pix := img.Pix
|
||||||
buf := getOutBuf(len(pixels) * 4)
|
buf := getOutBuf(len(pix))
|
||||||
for _, p := range pixels {
|
for o := 0; o < len(pix); o += 4 {
|
||||||
brightness := int(p) * 100 / 255
|
brightness := (int(pix[o])*299 + int(pix[o+1])*587 + int(pix[o+2])*114) / 255 / 10
|
||||||
index := rampIndex(brightness, options.brightnessThreshold, total, options.invert)
|
buf = append(buf, rampLUT[brightness]...)
|
||||||
buf = utf8.AppendRune(buf, ramp[index])
|
|
||||||
}
|
}
|
||||||
ascii := string(buf)
|
ascii := string(buf)
|
||||||
putOutBuf(buf)
|
putOutBuf(buf)
|
||||||
@@ -164,11 +165,18 @@ const ansiReset = "\x1b[0m"
|
|||||||
|
|
||||||
var ansi256Levels = [6]int{0, 95, 135, 175, 215, 255}
|
var ansi256Levels = [6]int{0, 95, 135, 175, 215, 255}
|
||||||
|
|
||||||
func quantize256(r, g, b uint8) int {
|
var decimal = func() (t [256]string) {
|
||||||
toLevel := func(v uint8) int {
|
for i := range t {
|
||||||
|
t[i] = strconv.Itoa(i)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
var ansi256Cube = func() (t [256]uint8) {
|
||||||
|
for v := range t {
|
||||||
best, bestDist := 0, 1<<30
|
best, bestDist := 0, 1<<30
|
||||||
for i, l := range ansi256Levels {
|
for i, l := range ansi256Levels {
|
||||||
d := int(v) - l
|
d := v - l
|
||||||
if d < 0 {
|
if d < 0 {
|
||||||
d = -d
|
d = -d
|
||||||
}
|
}
|
||||||
@@ -176,28 +184,31 @@ func quantize256(r, g, b uint8) int {
|
|||||||
bestDist, best = d, i
|
bestDist, best = d, i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return best
|
t[v] = uint8(best)
|
||||||
}
|
}
|
||||||
return 16 + 36*toLevel(r) + 6*toLevel(g) + toLevel(b)
|
return
|
||||||
|
}()
|
||||||
|
|
||||||
|
func quantize256(r, g, b uint8) int {
|
||||||
|
return 16 + 36*int(ansi256Cube[r]) + 6*int(ansi256Cube[g]) + int(ansi256Cube[b])
|
||||||
}
|
}
|
||||||
|
|
||||||
func appendColor(buf []byte, r, g, b uint8, tier colorTier) []byte {
|
func appendColor(buf []byte, r, g, b uint8, tier colorTier) []byte {
|
||||||
if tier == colorTierTrueColor {
|
if tier == colorTierTrueColor {
|
||||||
buf = append(buf, "\x1b[38;2;"...)
|
buf = append(buf, "\x1b[38;2;"...)
|
||||||
buf = strconv.AppendUint(buf, uint64(r), 10)
|
buf = append(buf, decimal[r]...)
|
||||||
buf = append(buf, ';')
|
buf = append(buf, ';')
|
||||||
buf = strconv.AppendUint(buf, uint64(g), 10)
|
buf = append(buf, decimal[g]...)
|
||||||
buf = append(buf, ';')
|
buf = append(buf, ';')
|
||||||
buf = strconv.AppendUint(buf, uint64(b), 10)
|
buf = append(buf, decimal[b]...)
|
||||||
} else {
|
} else {
|
||||||
buf = append(buf, "\x1b[38;5;"...)
|
buf = append(buf, "\x1b[38;5;"...)
|
||||||
buf = strconv.AppendUint(buf, uint64(quantize256(r, g, b)), 10)
|
buf = append(buf, decimal[quantize256(r, g, b)]...)
|
||||||
}
|
}
|
||||||
return append(buf, 'm')
|
return append(buf, 'm')
|
||||||
}
|
}
|
||||||
|
|
||||||
func frameToAnsi(img *image.NRGBA, ramp []rune, options asciiOptions, tier colorTier) string {
|
func frameToAnsi(img *image.RGBA, rampLUT *[101][]byte, tier colorTier) string {
|
||||||
total := len(ramp)
|
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
buf := getOutBuf(bounds.Dx() * bounds.Dy() * 16)
|
buf := getOutBuf(bounds.Dx() * bounds.Dy() * 16)
|
||||||
var lastR, lastG, lastB uint8
|
var lastR, lastG, lastB uint8
|
||||||
@@ -207,13 +218,12 @@ func frameToAnsi(img *image.NRGBA, ramp []rune, options asciiOptions, tier color
|
|||||||
o := img.PixOffset(x, y)
|
o := img.PixOffset(x, y)
|
||||||
r, g, bl := img.Pix[o], img.Pix[o+1], img.Pix[o+2]
|
r, g, bl := img.Pix[o], img.Pix[o+1], img.Pix[o+2]
|
||||||
brightness := (int(r)*299 + int(g)*587 + int(bl)*114) / 255 / 10
|
brightness := (int(r)*299 + int(g)*587 + int(bl)*114) / 255 / 10
|
||||||
index := rampIndex(brightness, options.brightnessThreshold, total, options.invert)
|
|
||||||
if first || r != lastR || g != lastG || bl != lastB {
|
if first || r != lastR || g != lastG || bl != lastB {
|
||||||
buf = appendColor(buf, r, g, bl, tier)
|
buf = appendColor(buf, r, g, bl, tier)
|
||||||
lastR, lastG, lastB = r, g, bl
|
lastR, lastG, lastB = r, g, bl
|
||||||
first = false
|
first = false
|
||||||
}
|
}
|
||||||
buf = utf8.AppendRune(buf, ramp[index])
|
buf = append(buf, rampLUT[brightness]...)
|
||||||
}
|
}
|
||||||
if y < bounds.Max.Y-1 {
|
if y < bounds.Max.Y-1 {
|
||||||
buf = append(buf, ansiReset+"\r\n"...)
|
buf = append(buf, ansiReset+"\r\n"...)
|
||||||
@@ -296,17 +306,22 @@ type FrameRenderer struct {
|
|||||||
setID int
|
setID int
|
||||||
colorFrames [][]byte
|
colorFrames [][]byte
|
||||||
options asciiOptions
|
options asciiOptions
|
||||||
ramp []rune
|
rampLUT *[101][]byte
|
||||||
cache *renderCache
|
cache *renderCache
|
||||||
|
|
||||||
|
inflightMu sync.Mutex
|
||||||
|
inflight map[string]chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFrameRenderer(setID int, colorFrames [][]byte, options asciiOptions, cache *renderCache) *FrameRenderer {
|
func newFrameRenderer(setID int, colorFrames [][]byte, options asciiOptions, cache *renderCache) *FrameRenderer {
|
||||||
|
ramp := []rune(resolveCharset(options.charset))
|
||||||
return &FrameRenderer{
|
return &FrameRenderer{
|
||||||
setID: setID,
|
setID: setID,
|
||||||
colorFrames: colorFrames,
|
colorFrames: colorFrames,
|
||||||
options: options,
|
options: options,
|
||||||
ramp: []rune(resolveCharset(options.charset)),
|
rampLUT: buildRampLUT(ramp, options),
|
||||||
cache: cache,
|
cache: cache,
|
||||||
|
inflight: make(map[string]chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,21 +331,41 @@ func (r *FrameRenderer) render(index, width, height int, keepAspectRatio bool, t
|
|||||||
return ascii, nil
|
return ascii, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
n := width * height
|
if r.cache != nil {
|
||||||
if tier != colorTierNone {
|
for {
|
||||||
n *= 4
|
r.inflightMu.Lock()
|
||||||
|
wait, ok := r.inflight[key]
|
||||||
|
if !ok {
|
||||||
|
done := make(chan struct{})
|
||||||
|
r.inflight[key] = done
|
||||||
|
r.inflightMu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
r.inflightMu.Lock()
|
||||||
|
delete(r.inflight, key)
|
||||||
|
r.inflightMu.Unlock()
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
r.inflightMu.Unlock()
|
||||||
|
<-wait
|
||||||
|
if ascii, ok := r.cache.get(key); ok {
|
||||||
|
return ascii, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pix := getPixBuf(n)
|
|
||||||
img, err := resizeFrame(r.colorFrames[index], pix, width, height, keepAspectRatio, tier)
|
pix := getPixBuf(4 * width * height)
|
||||||
|
img, err := resizeFrame(r.colorFrames[index], pix, width, height, keepAspectRatio)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
putPixBuf(pix)
|
putPixBuf(pix)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
var ascii string
|
var ascii string
|
||||||
if tier == colorTierNone {
|
if tier == colorTierNone {
|
||||||
ascii = frameToAscii(img.(*image.Gray).Pix, r.ramp, r.options)
|
ascii = frameToAscii(img, r.rampLUT)
|
||||||
} else {
|
} else {
|
||||||
ascii = frameToAnsi(img.(*image.NRGBA), r.ramp, r.options, tier)
|
ascii = frameToAnsi(img, r.rampLUT, tier)
|
||||||
}
|
}
|
||||||
putPixBuf(pix)
|
putPixBuf(pix)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func loadBenchSet(b *testing.B) *FramesContainer {
|
||||||
|
b.Helper()
|
||||||
|
matches, _ := filepath.Glob("../frames/*.tsf")
|
||||||
|
if len(matches) == 0 {
|
||||||
|
b.Skip("no .tsf frame set in ../frames")
|
||||||
|
}
|
||||||
|
fc, err := loadTSF(matches[0])
|
||||||
|
if err != nil {
|
||||||
|
b.Skip("failed to load frame set:", err)
|
||||||
|
}
|
||||||
|
return fc
|
||||||
|
}
|
||||||
|
|
||||||
|
func benchRender(b *testing.B, tier colorTier, w, h int) {
|
||||||
|
fc := loadBenchSet(b)
|
||||||
|
r := newFrameRenderer(0, fc.ColorFrames, asciiOptions{
|
||||||
|
brightnessThreshold: 40,
|
||||||
|
charset: "detailed",
|
||||||
|
}, nil)
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
if _, err := r.render(i%len(fc.ColorFrames), w, h, false, tier); err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRenderTrueColor(b *testing.B) { benchRender(b, colorTierTrueColor, 120, 40) }
|
||||||
|
func BenchmarkRender256(b *testing.B) { benchRender(b, colorTier256, 120, 40) }
|
||||||
|
func BenchmarkRenderGray(b *testing.B) { benchRender(b, colorTierNone, 120, 40) }
|
||||||
|
func BenchmarkRenderTrueBig(b *testing.B) { benchRender(b, colorTierTrueColor, 240, 70) }
|
||||||
+6
-1
@@ -457,6 +457,7 @@ func (s *Server) playVideo(
|
|||||||
currentFrame := 0
|
currentFrame := 0
|
||||||
loopCount := 0
|
loopCount := 0
|
||||||
lastW, lastH := 0, 0
|
lastW, lastH := 0, 0
|
||||||
|
var writeBuf []byte
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -488,7 +489,11 @@ func (s *Server) playVideo(
|
|||||||
prefix = clearScreen
|
prefix = clearScreen
|
||||||
lastW, lastH = w, h
|
lastW, lastH = w, h
|
||||||
}
|
}
|
||||||
if _, err := channel.Write([]byte(syncStart + prefix + ascii + syncEnd)); err != nil {
|
writeBuf = append(writeBuf[:0], syncStart...)
|
||||||
|
writeBuf = append(writeBuf, prefix...)
|
||||||
|
writeBuf = append(writeBuf, ascii...)
|
||||||
|
writeBuf = append(writeBuf, syncEnd...)
|
||||||
|
if _, err := channel.Write(writeBuf); err != nil {
|
||||||
closeSession()
|
closeSession()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user