From b3c1fb4ede5166084f3c481a2f7f6f9bf1a4037d Mon Sep 17 00:00:00 2001 From: gigirassy Date: Sun, 1 Mar 2026 00:36:40 +0100 Subject: [PATCH] implement possible macos fallback --- main.go | 127 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 92 insertions(+), 35 deletions(-) diff --git a/main.go b/main.go index cd44262..d7d8396 100644 --- a/main.go +++ b/main.go @@ -22,12 +22,15 @@ import ( ) const ( - defaultListen = ":3333" - defaultDir = "./images" - defaultWidth = 90 - cacheSuffixFmt = ".ansi.%d.txt" - allowedUA = "curl" - maxImageDim = 2000 + defaultListen = ":3333" + defaultDir = "./images" + defaultWidth = 80 + cacheSuffixFmt = ".ansi.%s.%d.txt" + allowedUA = "curl" + maxImageDim = 2000 + fallbackTag = "fallback" + truecolorTag = "tc" + forceFallbackEnv = "FORCE_FALLBACK" ) var ( @@ -48,14 +51,11 @@ func main() { log.Fatalf("failed to create images dir: %v", err) } - log.Printf("images directory: %s", imgDir) - log.Printf("pre-generating ascii caches at width=%d ...", defaultWidth) scanAndPreGen(defaultWidth) - refreshCachedPaths(defaultWidth) + refreshCachedPaths() http.HandleFunc("/", rootHandler) - log.Printf("listening on %s — only curl clients accepted; non-curl will be redirected to %s", listen, redirectURL) if err := http.ListenAndServe(listen, nil); err != nil { log.Fatalf("server failed: %v", err) } @@ -84,8 +84,10 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { } } + tag := chooseTag(ua) + cacheLock.RLock() - candidates := filteredCachedPaths(width) + candidates := filteredCachedPaths(width, tag) cacheLock.RUnlock() if len(candidates) == 0 { @@ -95,18 +97,16 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { return } imgFile := images[rand.Intn(len(images))] - cachePath := cacheFilePath(imgFile, width) + cachePath := cacheFilePath(imgFile, width, tag) - log.Printf("generating cache for %s (width=%d) on demand", imgFile, width) - if err := generateAndSaveASCII(imgFile, cachePath, width); err != nil { - log.Printf("generation error: %v", err) + if err := generateAndSaveASCII(imgFile, cachePath, width, tag); err != nil { http.Error(w, "failed to generate ascii\n", http.StatusInternalServerError) return } cacheLock.Lock() - refreshCachedPaths(width) - candidates = filteredCachedPaths(width) + refreshCachedPaths() + candidates = filteredCachedPaths(width, tag) cacheLock.Unlock() } @@ -123,13 +123,30 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { } defer f.Close() - w.Header().Set("Content-Type", "text/plain; charset=utf8") + w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.WriteHeader(http.StatusOK) if _, err := io.Copy(w, f); err != nil { log.Printf("write error: %v", err) } } +func chooseTag(ua string) string { + if os.Getenv(forceFallbackEnv) == "1" { + return fallbackTag + } + if isLikelyMac(ua) { + return fallbackTag + } + return truecolorTag +} + +func isLikelyMac(ua string) bool { + if strings.Contains(ua, "darwin") || strings.Contains(ua, "macintosh") || strings.Contains(ua, "mac os") || strings.Contains(ua, "macos") || strings.Contains(ua, "iphone") || strings.Contains(ua, "ipad") { + return true + } + return false +} + func listImageFiles() []string { var files []string _ = filepath.Walk(imgDir, func(path string, info os.FileInfo, err error) error { @@ -140,42 +157,40 @@ func listImageFiles() []string { switch ext { case ".jpg", ".jpeg", ".png", ".gif", ".bmp": files = append(files, path) - default: } return nil }) return files } -func cacheFilePath(imagePath string, width int) string { +func cacheFilePath(imagePath string, width int, tag string) string { dir := filepath.Dir(imagePath) base := filepath.Base(imagePath) ext := filepath.Ext(base) name := strings.TrimSuffix(base, ext) - return filepath.Join(dir, fmt.Sprintf("%s"+cacheSuffixFmt, name, width)) + return filepath.Join(dir, fmt.Sprintf("%s"+cacheSuffixFmt, name, tag, width)) } func scanAndPreGen(width int) { images := listImageFiles() for _, img := range images { - cache := cacheFilePath(img, width) - if _, err := os.Stat(cache); err == nil { - continue - } - if err := generateAndSaveASCII(img, cache, width); err != nil { - log.Printf("failed to generate for %s: %v", img, err) + for _, tag := range []string{truecolorTag, fallbackTag} { + cache := cacheFilePath(img, width, tag) + if _, err := os.Stat(cache); err == nil { + continue + } + _ = generateAndSaveASCII(img, cache, width, tag) } } } -func refreshCachedPaths(width int) { - suffix := fmt.Sprintf(cacheSuffixFmt, width) +func refreshCachedPaths() { found := []string{} _ = filepath.Walk(imgDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } - if strings.HasSuffix(path, suffix) { + if strings.Contains(path, ".ansi.") && strings.HasSuffix(path, ".txt") { found = append(found, path) } return nil @@ -186,8 +201,8 @@ func refreshCachedPaths(width int) { cacheLock.Unlock() } -func filteredCachedPaths(width int) []string { - suffix := fmt.Sprintf(cacheSuffixFmt, width) +func filteredCachedPaths(width int, tag string) []string { + suffix := fmt.Sprintf(cacheSuffixFmt, tag, width) out := make([]string, 0, len(cachedPaths)) for _, p := range cachedPaths { if strings.HasSuffix(p, suffix) { @@ -197,7 +212,7 @@ func filteredCachedPaths(width int) []string { return out } -func generateAndSaveASCII(imagePath, cachePath string, width int) error { +func generateAndSaveASCII(imagePath, cachePath string, width int, tag string) error { f, err := os.Open(imagePath) if err != nil { return err @@ -226,12 +241,17 @@ func generateAndSaveASCII(imagePath, cachePath string, width int) error { img = dst } - ansi := ImageToANSI(img, width) + var outStr string + if tag == truecolorTag { + outStr = ImageToANSI(img, width) + } else { + outStr = ImageToASCIIFallback(img, width) + } if err := os.MkdirAll(filepath.Dir(cachePath), 0755); err != nil { return err } - if err := os.WriteFile(cachePath, []byte(ansi), 0644); err != nil { + if err := os.WriteFile(cachePath, []byte(outStr), 0644); err != nil { return err } @@ -274,6 +294,43 @@ func ImageToANSI(src image.Image, cols int) string { return out.String() } +func ImageToASCIIFallback(src image.Image, cols int) string { + b := src.Bounds() + origW := b.Dx() + origH := b.Dy() + if origW == 0 || origH == 0 { + return "" + } + + ratio := 0.5 + charRowsFloat := (float64(origH) / float64(origW)) * float64(cols) * ratio + charRows := int(charRowsFloat) + if charRows < 1 { + charRows = 1 + } + targetH := charRows * 2 + targetW := cols + + dst := image.NewRGBA(image.Rect(0, 0, targetW, targetH)) + xdraw.ApproxBiLinear.Scale(dst, dst.Bounds(), src, src.Bounds(), xdraw.Over, nil) + + palette := []rune("MNHQ$OC?7>!:-;,. ") + var out bytes.Buffer + for y := 0; y < targetH; y += 2 { + for x := 0; x < targetW; x++ { + tr := colorAt(dst, x, y) + br := colorAt(dst, x, y+1) + lum := (int(tr.R)+int(tr.G)+int(tr.B)+int(br.R)+int(br.G)+int(br.B))/6 + idx := (lum * (len(palette) - 1)) / 255 + ch := palette[idx] + out.WriteRune(ch) + out.WriteRune(ch) + } + out.WriteString("\n") + } + return out.String() +} + type c8 struct{ R, G, B uint8 } func colorAt(img *image.RGBA, x, y int) c8 {