@@ -4,9 +4,11 @@ import (
44 "encoding/json"
55 "errors"
66 "fmt"
7+ "image"
78 "os"
89 "os/exec"
910 "path/filepath"
11+ "strings"
1012 "sync"
1113)
1214
@@ -28,55 +30,55 @@ type ImageMetadata struct {
2830 Orientation int `json:"orientation,omitempty"`
2931
3032 // Image Technical Details
31- BitDepth int `json:"bit_depth,omitempty"`
32- ColorSpace string `json:"color_space,omitempty"`
33- Compression string `json:"compression,omitempty"`
34- XResolution float64 `json:"x_resolution,omitempty"`
35- YResolution float64 `json:"y_resolution,omitempty"`
36- ResolutionUnit string `json:"resolution_unit,omitempty"`
37- ImageDescription string `json:"image_description,omitempty"`
38- UserComment string `json:"user_comment,omitempty"`
33+ BitDepth int `json:"bit_depth,omitempty"`
34+ ColorSpace string `json:"color_space,omitempty"`
35+ Compression string `json:"compression,omitempty"`
36+ XResolution float64 `json:"x_resolution,omitempty"`
37+ YResolution float64 `json:"y_resolution,omitempty"`
38+ ResolutionUnit string `json:"resolution_unit,omitempty"`
39+ ImageDescription string `json:"image_description,omitempty"`
40+ UserComment string `json:"user_comment,omitempty"`
3941
4042 // Extended Metadata
4143 Extended map [string ]any `json:"extended,omitempty"`
4244 HasExtended bool `json:"has_extended"`
4345
4446 // Camera Information
45- CameraMake string `json:"camera_make,omitempty"`
46- CameraModel string `json:"camera_model,omitempty"`
47- CameraSerialNumber string `json:"camera_serial_number,omitempty"`
48- LensMake string `json:"lens_make,omitempty"`
49- LensModel string `json:"lens_model,omitempty"`
50- LensSerialNumber string `json:"lens_serial_number,omitempty"`
51- LensFocalLengthMin string `json:"lens_focal_length_min,omitempty"`
52- LensFocalLengthMax string `json:"lens_focal_length_max,omitempty"`
53- FirmwareVersion string `json:"firmware_version,omitempty"`
47+ CameraMake string `json:"camera_make,omitempty"`
48+ CameraModel string `json:"camera_model,omitempty"`
49+ CameraSerialNumber string `json:"camera_serial_number,omitempty"`
50+ LensMake string `json:"lens_make,omitempty"`
51+ LensModel string `json:"lens_model,omitempty"`
52+ LensSerialNumber string `json:"lens_serial_number,omitempty"`
53+ LensFocalLengthMin string `json:"lens_focal_length_min,omitempty"`
54+ LensFocalLengthMax string `json:"lens_focal_length_max,omitempty"`
55+ FirmwareVersion string `json:"firmware_version,omitempty"`
5456
5557 // Camera Settings
56- FocalLength string `json:"focal_length,omitempty"`
57- Aperture string `json:"aperture,omitempty"` // F-number
58- ShutterSpeed string `json:"shutter_speed,omitempty"`
59- ISO string `json:"iso,omitempty"`
58+ FocalLength string `json:"focal_length,omitempty"`
59+ Aperture string `json:"aperture,omitempty"` // F-number
60+ ShutterSpeed string `json:"shutter_speed,omitempty"`
61+ ISO string `json:"iso,omitempty"`
6062 ExposureCompensation string `json:"exposure_compensation,omitempty"`
61- ExposureMode string `json:"exposure_mode,omitempty"`
62- ExposureProgram string `json:"exposure_program,omitempty"`
63- MeteringMode string `json:"metering_mode,omitempty"`
64- WhiteBalance string `json:"white_balance,omitempty"`
65- Flash string `json:"flash,omitempty"`
66- FlashMode string `json:"flash_mode,omitempty"`
67- LightSource string `json:"light_source,omitempty"`
68- SceneCaptureType string `json:"scene_capture_type,omitempty"`
69- SubjectDistance string `json:"subject_distance,omitempty"`
70- FocusMode string `json:"focus_mode,omitempty"`
71- DigitalZoomRatio string `json:"digital_zoom_ratio,omitempty"`
63+ ExposureMode string `json:"exposure_mode,omitempty"`
64+ ExposureProgram string `json:"exposure_program,omitempty"`
65+ MeteringMode string `json:"metering_mode,omitempty"`
66+ WhiteBalance string `json:"white_balance,omitempty"`
67+ Flash string `json:"flash,omitempty"`
68+ FlashMode string `json:"flash_mode,omitempty"`
69+ LightSource string `json:"light_source,omitempty"`
70+ SceneCaptureType string `json:"scene_capture_type,omitempty"`
71+ SubjectDistance string `json:"subject_distance,omitempty"`
72+ FocusMode string `json:"focus_mode,omitempty"`
73+ DigitalZoomRatio string `json:"digital_zoom_ratio,omitempty"`
7274
7375 // Timestamps
74- DateTime string `json:"date_time,omitempty"`
75- DateTimeOriginal string `json:"date_time_original,omitempty"`
76+ DateTime string `json:"date_time,omitempty"`
77+ DateTimeOriginal string `json:"date_time_original,omitempty"`
7678 DateTimeDigitized string `json:"date_time_digitized,omitempty"`
77- CreateDate string `json:"create_date,omitempty"`
78- ModifyDate string `json:"modify_date,omitempty"`
79- TimeZone string `json:"time_zone,omitempty"`
79+ CreateDate string `json:"create_date,omitempty"`
80+ ModifyDate string `json:"modify_date,omitempty"`
81+ TimeZone string `json:"time_zone,omitempty"`
8082
8183 // GPS Location
8284 GPSLatitude string `json:"gps_latitude,omitempty"`
@@ -221,7 +223,7 @@ func extractBasicMetadata(src string) (*ImageMetadata, error) {
221223 }
222224
223225 // Detect format
224- format , err := FormatFromFilename (src )
226+ formatName , contentType , err := detectFormatDetails (src )
225227 if err != nil {
226228 return nil , fmt .Errorf ("failed to detect format: %w" , err )
227229 }
@@ -239,8 +241,8 @@ func extractBasicMetadata(src string) (*ImageMetadata, error) {
239241 metadata := & ImageMetadata {
240242 FilePath : src ,
241243 FileName : filepath .Base (src ),
242- Format : formatToString ( format ) ,
243- ContentType : mimeFromFormat ( format ) ,
244+ Format : formatName ,
245+ ContentType : contentType ,
244246 Width : width ,
245247 Height : height ,
246248 AspectRatio : formatAspectRatio (width , height ),
@@ -253,6 +255,66 @@ func extractBasicMetadata(src string) (*ImageMetadata, error) {
253255 return metadata , nil
254256}
255257
258+ func detectFormatDetails (src string ) (string , string , error ) {
259+ if format , err := FormatFromFilename (src ); err == nil {
260+ return formatToString (format ), mimeFromFormat (format ), nil
261+ }
262+
263+ file , err := os .Open (src )
264+ if err != nil {
265+ return "" , "" , fmt .Errorf ("open file for format detection: %w" , err )
266+ }
267+ defer file .Close ()
268+
269+ _ , decodedFormat , err := image .DecodeConfig (file )
270+ if err != nil {
271+ return "" , "" , err
272+ }
273+
274+ return normalizeDecodedFormat (decodedFormat ), mimeFromDecodedFormat (decodedFormat ), nil
275+ }
276+
277+ func normalizeDecodedFormat (format string ) string {
278+ switch strings .ToLower (strings .TrimSpace (format )) {
279+ case "jpeg" , "jpg" :
280+ return "JPEG"
281+ case "png" :
282+ return "PNG"
283+ case "gif" :
284+ return "GIF"
285+ case "tif" , "tiff" :
286+ return "TIFF"
287+ case "bmp" :
288+ return "BMP"
289+ case "webp" :
290+ return "WEBP"
291+ default :
292+ if strings .TrimSpace (format ) == "" {
293+ return "Unknown"
294+ }
295+ return strings .ToUpper (strings .TrimSpace (format ))
296+ }
297+ }
298+
299+ func mimeFromDecodedFormat (format string ) string {
300+ switch strings .ToLower (strings .TrimSpace (format )) {
301+ case "jpeg" , "jpg" :
302+ return "image/jpeg"
303+ case "png" :
304+ return "image/png"
305+ case "gif" :
306+ return "image/gif"
307+ case "tif" , "tiff" :
308+ return "image/tiff"
309+ case "bmp" :
310+ return "image/bmp"
311+ case "webp" :
312+ return "image/webp"
313+ default :
314+ return "application/octet-stream"
315+ }
316+ }
317+
256318// extractWithExiftool executes exiftool and parses JSON output
257319func extractWithExiftool (src string ) (map [string ]any , error ) {
258320 // Run: exiftool -json -G -a -s <file>
0 commit comments