From 3ffe741c0b8032f65d3b5e1770b840dc34bbb72f Mon Sep 17 00:00:00 2001 From: ivar Date: Sat, 4 Apr 2026 16:41:39 +0200 Subject: feat: add govips ConvertAndResize wrapper Co-Authored-By: Claude Sonnet 4.6 --- internal/media/process.go | 46 +++++++++++++++++++++++++ internal/media/process_test.go | 78 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 internal/media/process.go create mode 100644 internal/media/process_test.go diff --git a/internal/media/process.go b/internal/media/process.go new file mode 100644 index 0000000..bb3ac89 --- /dev/null +++ b/internal/media/process.go @@ -0,0 +1,46 @@ +package media + +import ( + "fmt" + + "github.com/davidbyttow/govips/v2/vips" +) + +// ConvertAndResize loads the image at src, optionally resizes it to width pixels +// wide (maintaining aspect ratio), and exports it in the requested format. +// +// width <= 0 means no resize. format must be "webp" or "jpeg". +func ConvertAndResize(src string, width int, format string) ([]byte, error) { + if format != "webp" && format != "jpeg" { + return nil, fmt.Errorf("unsupported format %q: must be webp or jpeg", format) + } + + img, err := vips.NewImageFromFile(src) + if err != nil { + return nil, fmt.Errorf("load image %s: %w", src, err) + } + defer img.Close() + + if width > 0 && width < img.Width() { + scale := float64(width) / float64(img.Width()) + if err := img.Resize(scale, vips.KernelLanczos3); err != nil { + return nil, fmt.Errorf("resize: %w", err) + } + } + + switch format { + case "webp": + buf, _, err := img.ExportWebp(vips.NewWebpExportParams()) + if err != nil { + return nil, fmt.Errorf("export webp: %w", err) + } + return buf, nil + case "jpeg": + buf, _, err := img.ExportJpeg(vips.NewJpegExportParams()) + if err != nil { + return nil, fmt.Errorf("export jpeg: %w", err) + } + return buf, nil + } + return nil, fmt.Errorf("unsupported format %q", format) +} diff --git a/internal/media/process_test.go b/internal/media/process_test.go new file mode 100644 index 0000000..da9d4a8 --- /dev/null +++ b/internal/media/process_test.go @@ -0,0 +1,78 @@ +package media + +import ( + "image" + "image/png" + "os" + "path/filepath" + "testing" + + "github.com/davidbyttow/govips/v2/vips" +) + +func TestMain(m *testing.M) { + vips.Startup(nil) + code := m.Run() + vips.Shutdown() + os.Exit(code) +} + +// testPNG writes a 100×60 PNG to a temp file and returns its path. +func testPNG(t *testing.T) string { + t.Helper() + img := image.NewRGBA(image.Rect(0, 0, 100, 60)) + path := filepath.Join(t.TempDir(), "src.png") + f, err := os.Create(path) + if err != nil { + t.Fatalf("create test png: %v", err) + } + if err := png.Encode(f, img); err != nil { + t.Fatalf("encode test png: %v", err) + } + f.Close() + return path +} + +func TestConvertToWebP(t *testing.T) { + out, err := ConvertAndResize(testPNG(t), 0, "webp") + if err != nil { + t.Fatalf("ConvertAndResize: %v", err) + } + if len(out) == 0 { + t.Fatal("expected non-empty output") + } +} + +func TestConvertToJPEG(t *testing.T) { + out, err := ConvertAndResize(testPNG(t), 0, "jpeg") + if err != nil { + t.Fatalf("ConvertAndResize: %v", err) + } + if len(out) == 0 { + t.Fatal("expected non-empty output") + } +} + +func TestResizeAndConvert(t *testing.T) { + out, err := ConvertAndResize(testPNG(t), 50, "webp") + if err != nil { + t.Fatalf("ConvertAndResize resize: %v", err) + } + if len(out) == 0 { + t.Fatal("expected non-empty output") + } +} + +func TestConvertUnsupportedFormat(t *testing.T) { + _, err := ConvertAndResize(testPNG(t), 0, "avif") + if err == nil { + t.Fatal("expected error for unsupported format") + } +} + +func TestConvertMissingFile(t *testing.T) { + _, err := ConvertAndResize("/nonexistent/file.png", 0, "webp") + if err == nil { + t.Fatal("expected error for missing file") + } +} -- cgit v1.3