chore: resize receipt images on upload (max 2048px, JPEG 85%) to prevent SMTP attachment size rejections
This commit is contained in:
parent
44309dc189
commit
cc56d1ac9f
3 changed files with 61 additions and 0 deletions
1
go.mod
1
go.mod
|
|
@ -7,6 +7,7 @@ require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/jung-kurt/gofpdf v1.16.2
|
github.com/jung-kurt/gofpdf v1.16.2
|
||||||
|
golang.org/x/image v0.18.0
|
||||||
modernc.org/sqlite v1.37.1
|
modernc.org/sqlite v1.37.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -27,6 +27,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||||
|
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
|
||||||
|
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,12 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -19,6 +22,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
|
||||||
"github.com/cclohmar/ReceiptNext/internal/ai"
|
"github.com/cclohmar/ReceiptNext/internal/ai"
|
||||||
"github.com/cclohmar/ReceiptNext/internal/database"
|
"github.com/cclohmar/ReceiptNext/internal/database"
|
||||||
|
|
@ -101,6 +105,17 @@ func (h *ExpenseHandler) UploadReceipt(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resize the image (max 2048px, JPEG 85%) to keep attachment sizes manageable
|
||||||
|
// and prevent SMTP size-limit rejections when filing events with many receipts.
|
||||||
|
resized, resizeErr := resizeImage(fileData)
|
||||||
|
if resizeErr == nil && len(resized) > 0 {
|
||||||
|
fileData = resized
|
||||||
|
// If the original was not JPEG, update extension since output is always JPEG.
|
||||||
|
if ext != "jpg" && ext != "jpeg" {
|
||||||
|
ext = "jpg"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 6. Generate a UUID-based filename and ensure the storage directory exists.
|
// 6. Generate a UUID-based filename and ensure the storage directory exists.
|
||||||
filename := utils.NewUUID() + "." + ext
|
filename := utils.NewUUID() + "." + ext
|
||||||
storagePath := filepath.Join("storage", filename)
|
storagePath := filepath.Join("storage", filename)
|
||||||
|
|
@ -659,3 +674,46 @@ func detectImageExtension(data []byte) string {
|
||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Image processing helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// resizeImage resizes image data to a maximum of 2048 pixels on the longest
|
||||||
|
// side while maintaining aspect ratio. Output is always JPEG at 85% quality.
|
||||||
|
// Returns the original data unchanged if the image is already smaller, or if
|
||||||
|
// decoding/resizing fails (e.g. unsupported format like HEIC).
|
||||||
|
func resizeImage(data []byte) ([]byte, error) {
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bounds := img.Bounds()
|
||||||
|
w, h := bounds.Dx(), bounds.Dy()
|
||||||
|
|
||||||
|
const maxDim = 2048
|
||||||
|
if w <= maxDim && h <= maxDim {
|
||||||
|
return data, nil // already small enough
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintain aspect ratio.
|
||||||
|
var newW, newH int
|
||||||
|
if w > h {
|
||||||
|
newW = maxDim
|
||||||
|
newH = h * maxDim / w
|
||||||
|
} else {
|
||||||
|
newH = maxDim
|
||||||
|
newW = w * maxDim / h
|
||||||
|
}
|
||||||
|
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, newW, newH))
|
||||||
|
draw.CatmullRom.Scale(dst, dst.Bounds(), img, bounds, draw.Over, nil)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := jpeg.Encode(&buf, dst, &jpeg.Options{Quality: 85}); err != nil {
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue