package imageproc import ( "bytes" "context" "crypto/sha1" "encoding/hex" "fmt" "image" _ "image/gif" _ "image/jpeg" _ "image/png" "io" "net/http" "net/url" "os" "path/filepath" "strings" _ "golang.org/x/image/bmp" _ "golang.org/x/image/tiff" _ "golang.org/x/image/webp" ) type LoadedImage struct { Image image.Image Format string SourcePath string FromCache bool } func LoadImage(ctx context.Context, source, cacheDir string, client *http.Client) (*LoadedImage, error) { if strings.TrimSpace(source) == "" { return nil, fmt.Errorf("empty image source") } if isHTTPURL(source) { return loadRemoteImage(ctx, source, cacheDir, client) } return loadLocalImage(source) } func isHTTPURL(raw string) bool { u, err := url.Parse(raw) return err == nil && (u.Scheme == "http" || u.Scheme == "https") } func loadLocalImage(path string) (*LoadedImage, error) { file, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open image: %w", err) } defer file.Close() img, format, err := image.Decode(file) if err != nil { return nil, fmt.Errorf("decode image: %w", err) } return &LoadedImage{Image: img, Format: format, SourcePath: path}, nil } func loadRemoteImage(ctx context.Context, source, cacheDir string, client *http.Client) (*LoadedImage, error) { if client == nil { client = http.DefaultClient } if err := os.MkdirAll(cacheDir, 0o755); err != nil { return nil, fmt.Errorf("create image cache: %w", err) } cachePath := filepath.Join(cacheDir, cacheFileName(source)) if data, err := os.ReadFile(cachePath); err == nil { img, format, err := image.Decode(bytes.NewReader(data)) if err == nil { return &LoadedImage{Image: img, Format: format, SourcePath: cachePath, FromCache: true}, nil } _ = os.Remove(cachePath) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, source, nil) if err != nil { return nil, fmt.Errorf("create image request: %w", err) } req.Header.Set("User-Agent", "MaterialAnalyzer/1.0 (+offline preprocessing)") req.Header.Set("Accept", "image/webp,image/jpeg,image/png,image/gif,*/*;q=0.1") resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("download image: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { return nil, fmt.Errorf("download image returned HTTP %d", resp.StatusCode) } data, err := io.ReadAll(io.LimitReader(resp.Body, 25<<20)) if err != nil { return nil, fmt.Errorf("read image response: %w", err) } if len(data) == 0 { return nil, fmt.Errorf("downloaded image is empty") } img, format, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("decode downloaded image: %w", err) } if err := os.WriteFile(cachePath, data, 0o644); err != nil { return nil, fmt.Errorf("write image cache: %w", err) } return &LoadedImage{Image: img, Format: format, SourcePath: cachePath}, nil } func cacheFileName(source string) string { sum := sha1.Sum([]byte(source)) ext := ".img" if u, err := url.Parse(source); err == nil { if candidate := strings.ToLower(filepath.Ext(u.Path)); isImageExt(candidate) { ext = candidate } } return hex.EncodeToString(sum[:]) + ext } func isImageExt(ext string) bool { switch ext { case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".tif", ".tiff": return true default: return false } }