Skip to content

Commit 6e9aaf3

Browse files
committed
feat: setup webp support
1 parent 3e64ba2 commit 6e9aaf3

3 files changed

Lines changed: 151 additions & 40 deletions

File tree

io.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"golang.org/x/image/bmp"
1717
"golang.org/x/image/tiff"
18+
_ "golang.org/x/image/webp"
1819
)
1920

2021
type fileSystem interface {

metadata.go

Lines changed: 102 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -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
257319
func extractWithExiftool(src string) (map[string]any, error) {
258320
// Run: exiftool -json -G -a -s <file>

metadata_format_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package imgx
2+
3+
import "testing"
4+
5+
func TestNormalizeDecodedFormat(t *testing.T) {
6+
testCases := []struct {
7+
name string
8+
in string
9+
want string
10+
}{
11+
{name: "webp", in: "webp", want: "WEBP"},
12+
{name: "jpeg uppercase", in: "JPEG", want: "JPEG"},
13+
{name: "tif alias", in: "tif", want: "TIFF"},
14+
{name: "trimmed custom", in: " heic ", want: "HEIC"},
15+
{name: "empty", in: " ", want: "Unknown"},
16+
}
17+
18+
for _, tc := range testCases {
19+
t.Run(tc.name, func(t *testing.T) {
20+
got := normalizeDecodedFormat(tc.in)
21+
if got != tc.want {
22+
t.Fatalf("normalizeDecodedFormat(%q) = %q, want %q", tc.in, got, tc.want)
23+
}
24+
})
25+
}
26+
}
27+
28+
func TestMimeFromDecodedFormat(t *testing.T) {
29+
testCases := []struct {
30+
name string
31+
in string
32+
want string
33+
}{
34+
{name: "webp", in: "webp", want: "image/webp"},
35+
{name: "jpeg", in: "jpeg", want: "image/jpeg"},
36+
{name: "tiff alias", in: "tif", want: "image/tiff"},
37+
{name: "unknown", in: "heic", want: "application/octet-stream"},
38+
}
39+
40+
for _, tc := range testCases {
41+
t.Run(tc.name, func(t *testing.T) {
42+
got := mimeFromDecodedFormat(tc.in)
43+
if got != tc.want {
44+
t.Fatalf("mimeFromDecodedFormat(%q) = %q, want %q", tc.in, got, tc.want)
45+
}
46+
})
47+
}
48+
}

0 commit comments

Comments
 (0)