129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
|
|
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
|
||
|
|
}
|
||
|
|
}
|