diff --git a/server/channels/app/imaging/orientation.go b/server/channels/app/imaging/orientation.go index 98fcffac9dc..35a50fed75b 100644 --- a/server/channels/app/imaging/orientation.go +++ b/server/channels/app/imaging/orientation.go @@ -10,8 +10,8 @@ import ( "io" "strings" - "github.com/anthonynsimon/bild/transform" "github.com/bep/imagemeta" + "github.com/boxes-ltd/imaging" ) const ( @@ -41,19 +41,19 @@ var errStopDecoding = fmt.Errorf("stop decoding") func MakeImageUpright(img image.Image, orientation int) image.Image { switch orientation { case UprightMirrored: - return transform.FlipH(img) + return imaging.FlipH(img) case UpsideDown: - return transform.Rotate(img, 180, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Rotate180(img) case UpsideDownMirrored: - return transform.FlipV(img) + return imaging.FlipV(img) case RotatedCWMirrored: - return transform.Rotate(transform.FlipH(img), -90, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Transpose(img) case RotatedCCW: - return transform.Rotate(img, 90, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Rotate270(img) case RotatedCCWMirrored: - return transform.Rotate(transform.FlipV(img), -90, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Transverse(img) case RotatedCW: - return transform.Rotate(img, 270, &transform.RotationOptions{ResizeBounds: true}) + return imaging.Rotate90(img) default: return img } diff --git a/server/channels/app/imaging/orientation_test.go b/server/channels/app/imaging/orientation_test.go index 862343c659e..059af620cf7 100644 --- a/server/channels/app/imaging/orientation_test.go +++ b/server/channels/app/imaging/orientation_test.go @@ -197,3 +197,57 @@ func TestGetImageOrientation(t *testing.T) { }) } } + +func TestMakeImageUpright(t *testing.T) { + // Each case loads the canonical EXIF fixture for orientation N (the + // 128x128 quadrants pattern in its stored, uncorrected form), applies + // MakeImageUpright(., N), and asserts that the result has the same + // pixels as the upright reference. + tcs := []struct { + name string + orientation int + inputName string + }{ + {"Upright (no-op)", Upright, "quadrants-orientation-1.png"}, + {"UprightMirrored (FlipH)", UprightMirrored, "quadrants-orientation-2.png"}, + {"UpsideDown (Rotate180)", UpsideDown, "quadrants-orientation-3.png"}, + {"UpsideDownMirrored (FlipV)", UpsideDownMirrored, "quadrants-orientation-4.png"}, + {"RotatedCWMirrored (Transpose)", RotatedCWMirrored, "quadrants-orientation-5.png"}, + {"RotatedCCW (Rotate270)", RotatedCCW, "quadrants-orientation-6.png"}, + {"RotatedCCWMirrored (Transverse)", RotatedCCWMirrored, "quadrants-orientation-7.png"}, + {"RotatedCW (Rotate90)", RotatedCW, "quadrants-orientation-8.png"}, + // Unsupported orientations fall through to the default branch and + // return the input unchanged. Pass the upright fixture so the + // no-op result still equals the upright reference. + {"unsupported orientation", 99, "quadrants-orientation-1.png"}, + } + + imgDir, ok := fileutils.FindDir("tests/exif_samples") + require.True(t, ok) + + d, err := NewDecoder(DecoderOptions{}) + require.NoError(t, err) + require.NotNil(t, d) + + uprightFile, err := os.Open(filepath.Join(imgDir, "quadrants-orientation-1.png")) + require.NoError(t, err) + defer uprightFile.Close() + + uprightImg, format, err := d.Decode(uprightFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + inputFile, err := os.Open(filepath.Join(imgDir, tc.inputName)) + require.NoError(t, err) + defer inputFile.Close() + + inputImg, format, err := d.Decode(inputFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + requireSameImage(t, uprightImg, MakeImageUpright(inputImg, tc.orientation)) + }) + } +} diff --git a/server/channels/app/imaging/preview.go b/server/channels/app/imaging/preview.go index f4415f0e51c..0674d66a7b0 100644 --- a/server/channels/app/imaging/preview.go +++ b/server/channels/app/imaging/preview.go @@ -9,7 +9,7 @@ import ( "image" "image/jpeg" - "github.com/anthonynsimon/bild/transform" + "github.com/boxes-ltd/imaging" ) // GeneratePreview generates the preview for the given image. @@ -18,7 +18,7 @@ func GeneratePreview(img image.Image, width int) image.Image { w := img.Bounds().Dx() if w > width { - preview = Resize(img, width, 0, transform.Lanczos) + preview = imaging.Resize(img, width, 0, imaging.Lanczos) } return preview @@ -31,16 +31,16 @@ func GenerateThumbnail(img image.Image, targetWidth, targetHeight int) image.Ima // We keep aspect ratio and ensure the output dimensions are never higher than the provided targets. if width > height { - return Resize(img, targetWidth, 0, transform.Lanczos) + return imaging.Resize(img, targetWidth, 0, imaging.Lanczos) } - return Resize(img, 0, targetHeight, transform.Lanczos) + return imaging.Resize(img, 0, targetHeight, imaging.Lanczos) } // GenerateMiniPreviewImage generates the mini preview for the given image. func GenerateMiniPreviewImage(img image.Image, w, h, q int) ([]byte, error) { var buf bytes.Buffer - preview := Resize(img, w, h, transform.Lanczos) + preview := imaging.Resize(img, w, h, imaging.Lanczos) if err := jpeg.Encode(&buf, preview, &jpeg.Options{Quality: q}); err != nil { return nil, fmt.Errorf("failed to encode image to JPEG format: %w", err) } diff --git a/server/channels/app/imaging/preview_bench_test.go b/server/channels/app/imaging/preview_bench_test.go new file mode 100644 index 00000000000..4c30f21073e --- /dev/null +++ b/server/channels/app/imaging/preview_bench_test.go @@ -0,0 +1,124 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package imaging + +import ( + "image" + "image/color" + "image/draw" + "testing" +) + +func newRGBAImage(w, h int) *image.RGBA { + img := image.NewRGBA(image.Rect(0, 0, w, h)) + draw.Draw(img, img.Bounds(), image.NewUniform(color.RGBA{R: 100, G: 150, B: 200, A: 255}), image.Point{}, draw.Src) + return img +} + +func BenchmarkGeneratePreview(b *testing.B) { + cases := []struct { + name string + w, h int + targetWidth int + }{ + {"2000x1500 -> 1024", 2000, 1500, 1024}, + {"4000x3000 -> 1024", 4000, 3000, 1024}, + {"1024x768 -> 1024 (no-op)", 1024, 768, 1024}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + img := newRGBAImage(tc.w, tc.h) + b.ResetTimer() + for b.Loop() { + GeneratePreview(img, tc.targetWidth) + } + }) + } +} + +func BenchmarkGenerateThumbnail(b *testing.B) { + cases := []struct { + name string + w, h int + targetW, targetH int + }{ + {"2000x1500 landscape -> 120x100", 2000, 1500, 120, 100}, + {"1500x2000 portrait -> 120x100", 1500, 2000, 120, 100}, + {"4000x3000 landscape -> 120x100", 4000, 3000, 120, 100}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + img := newRGBAImage(tc.w, tc.h) + b.ResetTimer() + for b.Loop() { + GenerateThumbnail(img, tc.targetW, tc.targetH) + } + }) + } +} + +func BenchmarkGenerateMiniPreviewImage(b *testing.B) { + cases := []struct { + name string + w, h int + targetW, targetH int + quality int + }{ + {"2000x1500 -> 120x100 q50", 2000, 1500, 120, 100, 50}, + {"4000x3000 -> 120x100 q50", 4000, 3000, 120, 100, 50}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + img := newRGBAImage(tc.w, tc.h) + b.ResetTimer() + for b.Loop() { + _, _ = GenerateMiniPreviewImage(img, tc.targetW, tc.targetH, tc.quality) + } + }) + } +} + +func BenchmarkFillCenter(b *testing.B) { + cases := []struct { + name string + w, h int + targetW, targetH int + }{ + {"2000x1500 -> 120x100", 2000, 1500, 120, 100}, + {"4000x3000 -> 120x100", 4000, 3000, 120, 100}, + } + for _, tc := range cases { + b.Run(tc.name, func(b *testing.B) { + img := newRGBAImage(tc.w, tc.h) + b.ResetTimer() + for b.Loop() { + FillCenter(img, tc.targetW, tc.targetH) + } + }) + } +} + +func BenchmarkMakeImageUpright(b *testing.B) { + orientations := []struct { + name string + orientation int + }{ + {"Upright (no-op)", Upright}, + {"UpsideDown (rotate 180)", UpsideDown}, + {"RotatedCCW (rotate 270)", RotatedCCW}, + {"RotatedCW (rotate 90)", RotatedCW}, + {"UprightMirrored (flip H)", UprightMirrored}, + {"UpsideDownMirrored (flip V)", UpsideDownMirrored}, + {"RotatedCWMirrored (transpose)", RotatedCWMirrored}, + {"RotatedCCWMirrored (transverse)", RotatedCCWMirrored}, + } + img := newRGBAImage(2000, 1500) + for _, tc := range orientations { + b.Run(tc.name, func(b *testing.B) { + for b.Loop() { + MakeImageUpright(img, tc.orientation) + } + }) + } +} diff --git a/server/channels/app/imaging/preview_test.go b/server/channels/app/imaging/preview_test.go index 63534275960..f9226b9ffb5 100644 --- a/server/channels/app/imaging/preview_test.go +++ b/server/channels/app/imaging/preview_test.go @@ -4,11 +4,14 @@ package imaging import ( + "bytes" "image" - "image/color" + "os" + "path/filepath" "testing" - "github.com/anthonynsimon/bild/transform" + "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" + "github.com/stretchr/testify/require" ) @@ -78,95 +81,64 @@ func TestGenerateThumbnail(t *testing.T) { } } -func createTestImage(t *testing.T, width, height int) image.Image { - t.Helper() - img := image.NewNRGBA(image.Rect(0, 0, width, height)) - for y := range height { - for x := range width { - img.Set(x, y, color.NRGBA{uint8(x % 256), uint8(y % 256), 0, 255}) - } - } - return img +func TestGeneratePreview(t *testing.T) { + imgDir, ok := fileutils.FindDir("tests") + require.True(t, ok) + + d, err := NewDecoder(DecoderOptions{}) + require.NoError(t, err) + require.NotNil(t, d) + + inputFile, err := os.Open(filepath.Join(imgDir, "qa-data-graph.png")) + require.NoError(t, err) + defer inputFile.Close() + + inputImg, format, err := d.Decode(inputFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + expectedFile, err := os.Open(filepath.Join(imgDir, "preview_test_qa_data_graph_1024.png")) + require.NoError(t, err) + defer expectedFile.Close() + + expectedImg, format, err := d.Decode(expectedFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + preview := GeneratePreview(inputImg, 1024) + requireSameImage(t, expectedImg, preview) } -func TestResize(t *testing.T) { - for _, tc := range []struct { - name string - img image.Image - targetW int - targetH int - expectedW int - expectedH int - }{ - { - name: "zero target dimensions", - img: createTestImage(t, 100, 50), - targetW: 0, - targetH: 0, - expectedW: 0, - expectedH: 0, - }, - { - name: "negative target dimensions", - img: createTestImage(t, 100, 50), - targetW: -1, - targetH: 25, - expectedW: 0, - expectedH: 0, - }, - { - name: "zero source dimensions", - img: createTestImage(t, 0, 0), - targetW: 50, - targetH: 25, - expectedW: 0, - expectedH: 0, - }, - { - name: "preserve aspect ratio with width", - img: createTestImage(t, 100, 50), - targetW: 50, - targetH: 0, - expectedW: 50, - expectedH: 25, - }, - { - name: "preserve aspect ratio with width, height > width", - img: createTestImage(t, 50, 100), - targetW: 50, - targetH: 0, - expectedW: 50, - expectedH: 100, - }, - { - name: "preserve aspect ratio with height", - img: createTestImage(t, 100, 50), - targetW: 0, - targetH: 25, - expectedW: 50, - expectedH: 25, - }, - { - name: "preserve aspect ratio with height, height > width", - img: createTestImage(t, 50, 100), - targetW: 0, - targetH: 25, - expectedW: 13, - expectedH: 25, - }, - { - name: "valid target dimensions", - img: createTestImage(t, 100, 50), - targetW: 50, - targetH: 25, - expectedW: 50, - expectedH: 25, - }, - } { - t.Run(tc.name, func(t *testing.T) { - resizedImg := Resize(tc.img, tc.targetW, tc.targetH, transform.Lanczos) - require.Equal(t, tc.expectedW, resizedImg.Bounds().Dx()) - require.Equal(t, tc.expectedH, resizedImg.Bounds().Dy()) - }) - } +func TestGenerateMiniPreviewImage(t *testing.T) { + imgDir, ok := fileutils.FindDir("tests") + require.True(t, ok) + + d, err := NewDecoder(DecoderOptions{}) + require.NoError(t, err) + require.NotNil(t, d) + + inputFile, err := os.Open(filepath.Join(imgDir, "qa-data-graph.png")) + require.NoError(t, err) + defer inputFile.Close() + + inputImg, format, err := d.Decode(inputFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + expectedFile, err := os.Open(filepath.Join(imgDir, "mini_preview_test_qa_data_graph_16x16_q90.jpg")) + require.NoError(t, err) + defer expectedFile.Close() + + expectedImg, format, err := d.Decode(expectedFile) + require.NoError(t, err) + require.Equal(t, "jpeg", format) + + out, err := GenerateMiniPreviewImage(inputImg, 16, 16, 90) + require.NoError(t, err) + + actualImg, format, err := d.Decode(bytes.NewReader(out)) + require.NoError(t, err) + require.Equal(t, "jpeg", format) + + requireSameImage(t, expectedImg, actualImg) } diff --git a/server/channels/app/imaging/utils.go b/server/channels/app/imaging/utils.go index 0c2eb8bd228..8197cb30f52 100644 --- a/server/channels/app/imaging/utils.go +++ b/server/channels/app/imaging/utils.go @@ -6,10 +6,8 @@ package imaging import ( "image" "image/color" - "math" - "github.com/anthonynsimon/bild/clone" - "github.com/anthonynsimon/bild/transform" + "github.com/boxes-ltd/imaging" ) type rawImg interface { @@ -142,126 +140,14 @@ func FillImageTransparency(img image.Image, c color.Color) { } } -// CropAnchor cuts out a rectangular region with the specified size -// from the image using the specified anchor point and returns the cropped image. -// Adapted from github.com/disintegration/imaging -func CropCenter(img image.Image, w, h int) image.Image { - srcBounds := img.Bounds() - anchorPoint := image.Pt(srcBounds.Min.X+(srcBounds.Dx()-w)/2, srcBounds.Min.Y+(srcBounds.Dy()-h)/2) - r := image.Rect(0, 0, w, h).Add(anchorPoint) - b := srcBounds.Intersect(r) - return transform.Crop(img, b) -} - -// resizeAndCrop resizes the image to the smallest possible size that will cover the specified dimensions, -// crops the resized image to the specified dimensions using a centered anchor point and returns -// the transformed image. -// Adapted from github.com/disintegration/imaging -func resizeAndCropCenter(img image.Image, width, height int) image.Image { - dstW, dstH := width, height - - srcBounds := img.Bounds() - srcW := srcBounds.Dx() - srcH := srcBounds.Dy() - srcAspectRatio := float64(srcW) / float64(srcH) - dstAspectRatio := float64(dstW) / float64(dstH) - - var tmp image.Image - if srcAspectRatio < dstAspectRatio { - tmp = Resize(img, dstW, 0, transform.Lanczos) - } else { - tmp = Resize(img, 0, dstH, transform.Lanczos) - } - - return CropCenter(tmp, dstW, dstH) -} - // FillCenter creates an image with the specified dimensions and fills it with // the centered and scaled source image. -// To achieve the correct aspect ratio without stretching, the source image will be cropped. -// Adapted from github.com/disintegration/imaging -func FillCenter(img image.Image, dstW, dstH int) image.Image { - if dstW <= 0 || dstH <= 0 { - return &image.RGBA{} - } - - srcBounds := img.Bounds() - srcW := srcBounds.Dx() - srcH := srcBounds.Dy() - - if srcW <= 0 || srcH <= 0 { - return &image.RGBA{} - } - - if srcW == dstW && srcH == dstH { - return clone.AsShallowRGBA(img) - } - - return resizeAndCropCenter(img, dstW, dstH) +func FillCenter(img image.Image, w, h int) *image.NRGBA { + return imaging.Fill(img, w, h, imaging.Center, imaging.Lanczos) } -// Fit scales down the image to fit the specified -// maximum width and height and returns the transformed image. -// Adapted from github.com/disintegration/imaging -func Fit(img image.Image, maxW, maxH int) image.Image { - if maxW <= 0 || maxH <= 0 { - return &image.NRGBA{} - } - - srcBounds := img.Bounds() - srcW := srcBounds.Dx() - srcH := srcBounds.Dy() - - if srcW <= 0 || srcH <= 0 { - return &image.RGBA{} - } - - if srcW <= maxW && srcH <= maxH { - return clone.AsShallowRGBA(img) - } - - srcAspectRatio := float64(srcW) / float64(srcH) - maxAspectRatio := float64(maxW) / float64(maxH) - - var newW, newH int - if srcAspectRatio > maxAspectRatio { - newW = maxW - newH = int(float64(newW) / srcAspectRatio) - } else { - newH = maxH - newW = int(float64(newH) * srcAspectRatio) - } - - return Resize(img, newW, newH, transform.Lanczos) -} - -// Resize resizes the image to the specified width and height using the specified resampling filter and returns the transformed image. -// If one of width or height is 0, the image aspect ratio is preserved. -// Adapted from github.com/disintegration/imaging -func Resize(img image.Image, targetWidth, targetHeight int, filter transform.ResampleFilter) image.Image { - if targetWidth < 0 || targetHeight < 0 { - return &image.NRGBA{} - } - - if targetWidth == 0 && targetHeight == 0 { - return &image.NRGBA{} - } - - srcW := img.Bounds().Dx() - srcH := img.Bounds().Dy() - if srcW <= 0 || srcH <= 0 { - return &image.NRGBA{} - } - - // If new width or height is 0 then preserve aspect ratio, minimum 1px. - if targetWidth == 0 { - tmpW := float64(targetHeight) * float64(srcW) / float64(srcH) - targetWidth = int(math.Max(1.0, math.Floor(tmpW+0.5))) - } - if targetHeight == 0 { - tmpH := float64(targetWidth) * float64(srcH) / float64(srcW) - targetHeight = int(math.Max(1.0, math.Floor(tmpH+0.5))) - } - - return transform.Resize(img, targetWidth, targetHeight, filter) +// Fit scales down the image to fit within the specified maximum dimensions, +// preserving the aspect ratio. +func Fit(img image.Image, maxW, maxH int) *image.NRGBA { + return imaging.Fit(img, maxW, maxH, imaging.Lanczos) } diff --git a/server/channels/app/imaging/utils_test.go b/server/channels/app/imaging/utils_test.go index bf04b2f4e01..17fa7f8ef12 100644 --- a/server/channels/app/imaging/utils_test.go +++ b/server/channels/app/imaging/utils_test.go @@ -8,6 +8,7 @@ import ( "image" "image/color" "os" + "path/filepath" "testing" "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" @@ -15,6 +16,46 @@ import ( "github.com/stretchr/testify/require" ) +// requireSameImage asserts that want and got cover the same bounds and have +// identical RGBA values at every pixel. We compare decoded pixels rather than +// re-encoded byte streams because image/png is not byte-stable across Go +// versions even for identical pixel content. On mismatch we walk every pixel +// before failing so the report includes the total diff rate, not just the +// first divergence. +func requireSameImage(t *testing.T, want, got image.Image) { + t.Helper() + require.Equal(t, want.Bounds(), got.Bounds()) + b := got.Bounds() + total := b.Dx() * b.Dy() + var ( + diff int + firstX int + firstY int + firstWant [4]uint32 + firstGot [4]uint32 + ) + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + wr, wg, wb, wa := want.At(x, y).RGBA() + gr, gg, gb, ga := got.At(x, y).RGBA() + if wr == gr && wg == gg && wb == gb && wa == ga { + continue + } + if diff == 0 { + firstX, firstY = x, y + firstWant = [4]uint32{wr, wg, wb, wa} + firstGot = [4]uint32{gr, gg, gb, ga} + } + diff++ + } + } + if diff > 0 { + t.Fatalf("%d / %d pixels differ (%.2f%%); first at (%d, %d): want %v got %v", + diff, total, 100*float64(diff)/float64(total), + firstX, firstY, firstWant, firstGot) + } +} + func TestFillImageTransparency(t *testing.T) { tcs := []struct { name string @@ -117,233 +158,98 @@ func TestFillImageTransparency(t *testing.T) { }) } -func TestCropCenter(t *testing.T) { - imgDir, ok := fileutils.FindDir("tests") - require.True(t, ok) - - d, err := NewDecoder(DecoderOptions{}) - require.NotNil(t, d) - require.NoError(t, err) - - for _, tc := range []struct { +func TestFillCenter(t *testing.T) { + tcs := []struct { name string - inputName string outputName string width int height int }{ - { - "Crop to center 100x100", - "crop_test_input.png", - "crop_test_output_100x100.png", - 100, - 100, - }, - { - "Crop to center 45x45", - "crop_test_input.png", - "crop_test_output_45x45.png", - 45, - 45, - }, - { - "Crop to center 100x45", - "crop_test_input.png", - "crop_test_output_100x45.png", - 100, - 45, - }, - { - "Crop to center 45x100", - "crop_test_input.png", - "crop_test_output_45x100.png", - 45, - 100, - }, - } { + {"100x100", "fill_test_output_100x100.png", 100, 100}, + {"45x45", "fill_test_output_45x45.png", 45, 45}, + {"100x45", "fill_test_output_100x45.png", 100, 45}, + {"45x100", "fill_test_output_45x100.png", 45, 100}, + } + + imgDir, ok := fileutils.FindDir("tests") + require.True(t, ok) + + d, err := NewDecoder(DecoderOptions{}) + require.NoError(t, err) + require.NotNil(t, d) + + inputFile, err := os.Open(filepath.Join(imgDir, "fill_test_input.png")) + require.NoError(t, err) + defer inputFile.Close() + + inputImg, format, err := d.Decode(inputFile) + require.NoError(t, err) + require.Equal(t, "png", format) + + for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - inputFile, err := os.Open(imgDir + "/" + tc.inputName) + expectedFile, err := os.Open(filepath.Join(imgDir, tc.outputName)) require.NoError(t, err) - require.NotNil(t, inputFile) - defer func() { - require.NoError(t, inputFile.Close()) - }() - - inputImg, format, err := d.Decode(inputFile) - require.NoError(t, err) - require.NotNil(t, inputImg) - require.Equal(t, "png", format) - - expectedFile, err := os.Open(imgDir + "/" + tc.outputName) - require.NoError(t, err) - require.NotNil(t, expectedFile) - defer func() { - require.NoError(t, expectedFile.Close()) - }() + defer expectedFile.Close() expectedImg, format, err := d.Decode(expectedFile) require.NoError(t, err) - require.NotNil(t, expectedImg) require.Equal(t, "png", format) - croppedImg := CropCenter(inputImg, tc.width, tc.height) - require.Equal(t, expectedImg.Bounds().Dx(), croppedImg.Bounds().Dx()) - require.Equal(t, expectedImg.Bounds().Dy(), croppedImg.Bounds().Dy()) - require.Equal(t, expectedImg.(*image.RGBA).Pix, croppedImg.(*image.RGBA).Pix) + out := FillCenter(inputImg, tc.width, tc.height) + requireSameImage(t, expectedImg, out) }) } } func TestFit(t *testing.T) { - imgDir, ok := fileutils.FindDir("tests") - require.True(t, ok) - - d, err := NewDecoder(DecoderOptions{}) - require.NotNil(t, d) - require.NoError(t, err) - - for _, tc := range []struct { - name string - inputName string - outputName string - width int - height int - }{ - { - "Fit to 100x100", - "fit_test_input.png", - "fit_test_output_100x100.png", - 100, - 100, - }, - { - "Fit to 45x45", - "fit_test_input.png", - "fit_test_output_45x45.png", - 45, - 45, - }, - { - "Fit to 100x45", - "fit_test_input.png", - "fit_test_output_100x45.png", - 100, - 45, - }, - { - "Fit to 45x100", - "fit_test_input.png", - "fit_test_output_45x100.png", - 45, - 100, - }, - } { - t.Run(tc.name, func(t *testing.T) { - inputFile, err := os.Open(imgDir + "/" + tc.inputName) - require.NoError(t, err) - require.NotNil(t, inputFile) - defer func() { - require.NoError(t, inputFile.Close()) - }() - - inputImg, format, err := d.Decode(inputFile) - require.NoError(t, err) - require.NotNil(t, inputImg) - require.Equal(t, "png", format) - - expectedFile, err := os.Open(imgDir + "/" + tc.outputName) - require.NoError(t, err) - require.NotNil(t, expectedFile) - defer func() { - require.NoError(t, expectedFile.Close()) - }() - - expectedImg, format, err := d.Decode(expectedFile) - require.NoError(t, err) - require.NotNil(t, expectedImg) - require.Equal(t, "png", format) - - fittedImg := Fit(inputImg, tc.width, tc.height) - require.Equal(t, expectedImg, fittedImg) - }) - } -} - -func TestFillCenter(t *testing.T) { - imgDir, ok := fileutils.FindDir("tests") - require.True(t, ok) - - d, err := NewDecoder(DecoderOptions{}) - require.NotNil(t, d) - require.NoError(t, err) - tcs := []struct { - name string - inputName string - outputName string - width int - height int + name string + inputImg image.Image + maxW int + maxH int + expectedWidth int + expectedHeight int }{ { - "Fill center 100x100", - "fill_test_input.png", - "fill_test_output_100x100.png", - 100, - 100, + name: "no resize when smaller than bounds", + inputImg: image.NewRGBA(image.Rect(0, 0, 50, 50)), + maxW: 100, + maxH: 100, + expectedWidth: 50, + expectedHeight: 50, }, { - "Fill center 45x45", - "fill_test_input.png", - "fill_test_output_45x45.png", - 45, - 45, + name: "landscape clamps to width", + inputImg: image.NewRGBA(image.Rect(0, 0, 200, 100)), + maxW: 100, + maxH: 100, + expectedWidth: 100, + expectedHeight: 50, }, { - "Fill center 100x45", - "fill_test_input.png", - "fill_test_output_100x45.png", - 100, - 45, + name: "portrait clamps to height", + inputImg: image.NewRGBA(image.Rect(0, 0, 100, 200)), + maxW: 100, + maxH: 100, + expectedWidth: 50, + expectedHeight: 100, }, { - "Fill center 45x100", - "fill_test_input.png", - "fill_test_output_45x100.png", - 45, - 100, + name: "both dimensions exceed", + inputImg: image.NewRGBA(image.Rect(0, 0, 400, 200)), + maxW: 100, + maxH: 100, + expectedWidth: 100, + expectedHeight: 50, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { - inputFile, err := os.Open(imgDir + "/" + tc.inputName) - require.NoError(t, err) - require.NotNil(t, inputFile) - defer func() { - require.NoError(t, inputFile.Close()) - }() - - inputImg, format, err := d.Decode(inputFile) - require.NoError(t, err) - require.NotNil(t, inputImg) - require.Equal(t, "png", format) - - expectedFile, err := os.Open(imgDir + "/" + tc.outputName) - require.NoError(t, err) - require.NotNil(t, expectedFile) - defer func() { - require.NoError(t, expectedFile.Close()) - }() - - expectedImg, format, err := d.Decode(expectedFile) - require.NoError(t, err) - require.NotNil(t, expectedImg) - require.Equal(t, "png", format) - - filledImg := FillCenter(inputImg, tc.width, tc.height) - require.Equal(t, expectedImg.Bounds().Dx(), filledImg.Bounds().Dx()) - require.Equal(t, expectedImg.Bounds().Dy(), filledImg.Bounds().Dy()) - require.Equal(t, expectedImg.(*image.RGBA).Pix, filledImg.(*image.RGBA).Pix) + out := Fit(tc.inputImg, tc.maxW, tc.maxH) + require.Equal(t, tc.expectedWidth, out.Bounds().Dx()) + require.Equal(t, tc.expectedHeight, out.Bounds().Dy()) }) } } diff --git a/server/go.mod b/server/go.mod index b2407cba7a0..1be1ca65204 100644 --- a/server/go.mod +++ b/server/go.mod @@ -5,7 +5,7 @@ go 1.25.9 require ( code.sajari.com/docconv/v2 v2.0.0-pre.4 github.com/Masterminds/semver/v3 v3.4.0 - github.com/anthonynsimon/bild v0.14.0 + github.com/boxes-ltd/imaging v1.7.5 github.com/avct/uasurfer v0.0.0-20250915105040-a942f6fb6edc github.com/aws/aws-sdk-go-v2 v1.41.5 github.com/aws/aws-sdk-go-v2/config v1.32.13 diff --git a/server/go.sum b/server/go.sum index bbef0ad28e6..e7c99b8a9e3 100644 --- a/server/go.sum +++ b/server/go.sum @@ -36,8 +36,6 @@ github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9Pq github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/anthonynsimon/bild v0.14.0 h1:IFRkmKdNdqmexXHfEU7rPlAmdUZ8BDZEGtGHDnGWync= -github.com/anthonynsimon/bild v0.14.0/go.mod h1:hcvEAyBjTW69qkKJTfpcDQ83sSZHxwOunsseDfeQhUs= github.com/araddon/dateparse v0.0.0-20180729174819-cfd92a431d0e/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA= github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw= @@ -99,6 +97,8 @@ github.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4 github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8= github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4= github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= +github.com/boxes-ltd/imaging v1.7.5 h1:k4kYxJEhysoGhEEN1IEeKoSbnG8/8snjj7M48Ok0fnk= +github.com/boxes-ltd/imaging v1.7.5/go.mod h1:+8H+oRvis3InOFtTpcoCCB1RDXqo6p9tQBtjZfWnrC8= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= diff --git a/server/tests/crop_test_input.png b/server/tests/crop_test_input.png deleted file mode 100644 index 04f885b319e..00000000000 Binary files a/server/tests/crop_test_input.png and /dev/null differ diff --git a/server/tests/crop_test_output_100x100.png b/server/tests/crop_test_output_100x100.png deleted file mode 100644 index a01245624ec..00000000000 Binary files a/server/tests/crop_test_output_100x100.png and /dev/null differ diff --git a/server/tests/crop_test_output_100x45.png b/server/tests/crop_test_output_100x45.png deleted file mode 100644 index 34ef2eb9906..00000000000 Binary files a/server/tests/crop_test_output_100x45.png and /dev/null differ diff --git a/server/tests/crop_test_output_45x100.png b/server/tests/crop_test_output_45x100.png deleted file mode 100644 index d6c58d428eb..00000000000 Binary files a/server/tests/crop_test_output_45x100.png and /dev/null differ diff --git a/server/tests/crop_test_output_45x45.png b/server/tests/crop_test_output_45x45.png deleted file mode 100644 index e33bbba2fe6..00000000000 Binary files a/server/tests/crop_test_output_45x45.png and /dev/null differ diff --git a/server/tests/exif_samples/quadrants-orientation-2.png b/server/tests/exif_samples/quadrants-orientation-2.png new file mode 100644 index 00000000000..30fa30941dc Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-2.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-3.png b/server/tests/exif_samples/quadrants-orientation-3.png new file mode 100644 index 00000000000..f635ec40239 Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-3.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-4.png b/server/tests/exif_samples/quadrants-orientation-4.png new file mode 100644 index 00000000000..41c7fb8cb64 Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-4.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-5.png b/server/tests/exif_samples/quadrants-orientation-5.png new file mode 100644 index 00000000000..59967716e2e Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-5.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-6.png b/server/tests/exif_samples/quadrants-orientation-6.png new file mode 100644 index 00000000000..c04973877dc Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-6.png differ diff --git a/server/tests/exif_samples/quadrants-orientation-7.png b/server/tests/exif_samples/quadrants-orientation-7.png new file mode 100644 index 00000000000..bf1e4bcff18 Binary files /dev/null and b/server/tests/exif_samples/quadrants-orientation-7.png differ diff --git a/server/tests/fill_test_output_100x100.png b/server/tests/fill_test_output_100x100.png index 514e639a27c..736a6e6e170 100644 Binary files a/server/tests/fill_test_output_100x100.png and b/server/tests/fill_test_output_100x100.png differ diff --git a/server/tests/fill_test_output_100x45.png b/server/tests/fill_test_output_100x45.png index b0ed90495ea..b624b4c926b 100644 Binary files a/server/tests/fill_test_output_100x45.png and b/server/tests/fill_test_output_100x45.png differ diff --git a/server/tests/fill_test_output_45x100.png b/server/tests/fill_test_output_45x100.png index 9e8c37b5a9c..9e0c5d0de18 100644 Binary files a/server/tests/fill_test_output_45x100.png and b/server/tests/fill_test_output_45x100.png differ diff --git a/server/tests/fill_test_output_45x45.png b/server/tests/fill_test_output_45x45.png index 696d8b0a5a2..12630e1588b 100644 Binary files a/server/tests/fill_test_output_45x45.png and b/server/tests/fill_test_output_45x45.png differ diff --git a/server/tests/fit_test_input.png b/server/tests/fit_test_input.png deleted file mode 100644 index 50b76a159ba..00000000000 Binary files a/server/tests/fit_test_input.png and /dev/null differ diff --git a/server/tests/fit_test_output_100x100.png b/server/tests/fit_test_output_100x100.png deleted file mode 100644 index 4ffee7a51f3..00000000000 Binary files a/server/tests/fit_test_output_100x100.png and /dev/null differ diff --git a/server/tests/fit_test_output_100x45.png b/server/tests/fit_test_output_100x45.png deleted file mode 100644 index 437ab0a4ce7..00000000000 Binary files a/server/tests/fit_test_output_100x45.png and /dev/null differ diff --git a/server/tests/fit_test_output_45x100.png b/server/tests/fit_test_output_45x100.png deleted file mode 100644 index 3e3521acb00..00000000000 Binary files a/server/tests/fit_test_output_45x100.png and /dev/null differ diff --git a/server/tests/fit_test_output_45x45.png b/server/tests/fit_test_output_45x45.png deleted file mode 100644 index 3e3521acb00..00000000000 Binary files a/server/tests/fit_test_output_45x45.png and /dev/null differ diff --git a/server/tests/mini_preview_test_qa_data_graph_16x16_q90.jpg b/server/tests/mini_preview_test_qa_data_graph_16x16_q90.jpg new file mode 100644 index 00000000000..e2ac536b280 Binary files /dev/null and b/server/tests/mini_preview_test_qa_data_graph_16x16_q90.jpg differ diff --git a/server/tests/orientation_test_2_expected_preview.jpeg b/server/tests/orientation_test_2_expected_preview.jpeg index 769e4e19300..262510eb822 100644 Binary files a/server/tests/orientation_test_2_expected_preview.jpeg and b/server/tests/orientation_test_2_expected_preview.jpeg differ diff --git a/server/tests/orientation_test_3_expected_preview.jpeg b/server/tests/orientation_test_3_expected_preview.jpeg index 26aeb04a256..ab59f368be5 100644 Binary files a/server/tests/orientation_test_3_expected_preview.jpeg and b/server/tests/orientation_test_3_expected_preview.jpeg differ diff --git a/server/tests/orientation_test_6_expected_preview.jpeg b/server/tests/orientation_test_6_expected_preview.jpeg index fecf18955df..5909639e62d 100644 Binary files a/server/tests/orientation_test_6_expected_preview.jpeg and b/server/tests/orientation_test_6_expected_preview.jpeg differ diff --git a/server/tests/orientation_test_7_expected_preview.jpeg b/server/tests/orientation_test_7_expected_preview.jpeg index 7f5b5198046..8ea4c2ae440 100644 Binary files a/server/tests/orientation_test_7_expected_preview.jpeg and b/server/tests/orientation_test_7_expected_preview.jpeg differ diff --git a/server/tests/orientation_test_8_expected_preview.jpeg b/server/tests/orientation_test_8_expected_preview.jpeg index 6b5803de0f9..bfe7b7a6388 100644 Binary files a/server/tests/orientation_test_8_expected_preview.jpeg and b/server/tests/orientation_test_8_expected_preview.jpeg differ diff --git a/server/tests/preview_test_qa_data_graph_1024.png b/server/tests/preview_test_qa_data_graph_1024.png new file mode 100644 index 00000000000..891cbd08ddc Binary files /dev/null and b/server/tests/preview_test_qa_data_graph_1024.png differ diff --git a/server/tests/testgif_expected_thumbnail.jpg b/server/tests/testgif_expected_thumbnail.jpg index 967a6589be5..47711fd2ede 100644 Binary files a/server/tests/testgif_expected_thumbnail.jpg and b/server/tests/testgif_expected_thumbnail.jpg differ