diff --git a/main.go b/main.go index d7d8396..cd44262 100644 --- a/main.go +++ b/main.go @@ -22,15 +22,12 @@ import ( ) const ( - defaultListen = ":3333" - defaultDir = "./images" - defaultWidth = 80 - cacheSuffixFmt = ".ansi.%s.%d.txt" - allowedUA = "curl" - maxImageDim = 2000 - fallbackTag = "fallback" - truecolorTag = "tc" - forceFallbackEnv = "FORCE_FALLBACK" + defaultListen = ":3333" + defaultDir = "./images" + defaultWidth = 90 + cacheSuffixFmt = ".ansi.%d.txt" + allowedUA = "curl" + maxImageDim = 2000 ) var ( @@ -51,11 +48,14 @@ 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() + refreshCachedPaths(defaultWidth) 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,10 +84,8 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { } } - tag := chooseTag(ua) - cacheLock.RLock() - candidates := filteredCachedPaths(width, tag) + candidates := filteredCachedPaths(width) cacheLock.RUnlock() if len(candidates) == 0 { @@ -97,16 +95,18 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { return } imgFile := images[rand.Intn(len(images))] - cachePath := cacheFilePath(imgFile, width, tag) + cachePath := cacheFilePath(imgFile, width) - if err := generateAndSaveASCII(imgFile, cachePath, width, tag); err != nil { + 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) http.Error(w, "failed to generate ascii\n", http.StatusInternalServerError) return } cacheLock.Lock() - refreshCachedPaths() - candidates = filteredCachedPaths(width, tag) + refreshCachedPaths(width) + candidates = filteredCachedPaths(width) cacheLock.Unlock() } @@ -123,30 +123,13 @@ func rootHandler(w http.ResponseWriter, r *http.Request) { } defer f.Close() - w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Content-Type", "text/plain; charset=utf8") 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 { @@ -157,40 +140,42 @@ 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, tag string) string { +func cacheFilePath(imagePath string, width int) 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, tag, width)) + return filepath.Join(dir, fmt.Sprintf("%s"+cacheSuffixFmt, name, width)) } func scanAndPreGen(width int) { images := listImageFiles() for _, img := range images { - for _, tag := range []string{truecolorTag, fallbackTag} { - cache := cacheFilePath(img, width, tag) - if _, err := os.Stat(cache); err == nil { - continue - } - _ = generateAndSaveASCII(img, cache, width, tag) + 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) } } } -func refreshCachedPaths() { +func refreshCachedPaths(width int) { + suffix := fmt.Sprintf(cacheSuffixFmt, width) found := []string{} _ = filepath.Walk(imgDir, func(path string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } - if strings.Contains(path, ".ansi.") && strings.HasSuffix(path, ".txt") { + if strings.HasSuffix(path, suffix) { found = append(found, path) } return nil @@ -201,8 +186,8 @@ func refreshCachedPaths() { cacheLock.Unlock() } -func filteredCachedPaths(width int, tag string) []string { - suffix := fmt.Sprintf(cacheSuffixFmt, tag, width) +func filteredCachedPaths(width int) []string { + suffix := fmt.Sprintf(cacheSuffixFmt, width) out := make([]string, 0, len(cachedPaths)) for _, p := range cachedPaths { if strings.HasSuffix(p, suffix) { @@ -212,7 +197,7 @@ func filteredCachedPaths(width int, tag string) []string { return out } -func generateAndSaveASCII(imagePath, cachePath string, width int, tag string) error { +func generateAndSaveASCII(imagePath, cachePath string, width int) error { f, err := os.Open(imagePath) if err != nil { return err @@ -241,17 +226,12 @@ func generateAndSaveASCII(imagePath, cachePath string, width int, tag string) er img = dst } - var outStr string - if tag == truecolorTag { - outStr = ImageToANSI(img, width) - } else { - outStr = ImageToASCIIFallback(img, width) - } + ansi := ImageToANSI(img, width) if err := os.MkdirAll(filepath.Dir(cachePath), 0755); err != nil { return err } - if err := os.WriteFile(cachePath, []byte(outStr), 0644); err != nil { + if err := os.WriteFile(cachePath, []byte(ansi), 0644); err != nil { return err } @@ -294,43 +274,6 @@ 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 {