package photos import ( "bytes" "crypto/sha256" "encoding/hex" "fmt" "image" "strconv" "strings" "time" exif "github.com/dsoprea/go-exif/v3" exifcommon "github.com/dsoprea/go-exif/v3/common" "github.com/disintegration/imaging" "github.com/gen2brain/webp" ) type ImageMeta struct { Width int Height int MimeType string TakenAt *time.Time Latitude *float64 Longitude *float64 Thumb []byte Preview []byte } func inspect(data []byte) (width, height int, ok bool) { cfg, _, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { return 0, 0, false } return cfg.Width, cfg.Height, true } type exifInfo struct { TakenAt *time.Time Latitude *float64 Longitude *float64 Attributes map[string]any } func collectTags(ifd *exif.Ifd, dst map[string]any) { if ifd == nil { return } for _, ite := range ifd.Entries() { name := ite.TagName() if name == "" || strings.HasPrefix(name, "UnknownTag") { continue } if _, exists := dst[name]; exists { continue } s, err := ite.FormatFirst() if err != nil { continue } s = strings.TrimSpace(strings.TrimRight(s, "\x00")) if s == "" { continue } dst[name] = normalizeExifValue(s) } for _, child := range ifd.Children() { collectTags(child, dst) } } func normalizeExifValue(s string) any { base := strings.TrimSuffix(s, "...") if i := strings.IndexByte(base, '/'); i > 0 { num, err1 := strconv.ParseInt(strings.TrimSpace(base[:i]), 10, 64) den, err2 := strconv.ParseInt(strings.TrimSpace(base[i+1:]), 10, 64) if err1 == nil && err2 == nil && den != 0 { return float64(num) / float64(den) } } if n, err := strconv.ParseInt(base, 10, 64); err == nil { return n } if f, err := strconv.ParseFloat(base, 64); err == nil { return f } return s } func extractEXIF(data []byte) exifInfo { out := exifInfo{Attributes: map[string]any{}} raw, err := exif.SearchAndExtractExif(data) if err != nil { return out } im, err := exifcommon.NewIfdMappingWithStandard() if err != nil { return out } ti := exif.NewTagIndex() _, index, err := exif.Collect(im, ti, raw) if err != nil || index.RootIfd == nil { return out } collectTags(index.RootIfd, out.Attributes) if v, ok := out.Attributes["DateTimeOriginal"]; ok { if t, err := time.Parse("2006:01:02 15:04:05", fmt.Sprintf("%v", v)); err == nil { utc := t.UTC() out.TakenAt = &utc } } if gi, err := index.RootIfd.GpsInfo(); err == nil { lat := gi.Latitude.Decimal() lon := gi.Longitude.Decimal() out.Latitude, out.Longitude = &lat, &lon } return out } func ExtractEXIFAttributes(data []byte) map[string]any { return extractEXIF(data).Attributes } func buildVariants(data []byte) (thumb, preview []byte, err error) { img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true)) if err != nil { return nil, nil, err } thumbImg := imaging.Fit(img, 200, 200, imaging.Lanczos) previewImg := imaging.Fit(img, 1200, 1200, imaging.Lanczos) opts := webp.Options{Quality: 82} var thumbBuf bytes.Buffer if err := webp.Encode(&thumbBuf, thumbImg, opts); err != nil { return nil, nil, err } var previewBuf bytes.Buffer if err := webp.Encode(&previewBuf, previewImg, opts); err != nil { return nil, nil, err } return thumbBuf.Bytes(), previewBuf.Bytes(), nil } // checksum returns the hex-encoded SHA-256 of the given bytes. func checksum(data []byte) string { sum := sha256.Sum256(data) return hex.EncodeToString(sum[:]) }