浏览代码

Merge pull request #84 from bspammer/crop_before_resize

Crop before resize in Fill
Grigory Dryapak 6 年之前
父节点
当前提交
b5a2b9828d
共有 6 个文件被更改,包括 110 次插入8 次删除
  1. 47 8
      resize.go
  2. 63 0
      resize_test.go
  3. 二进制
      testdata/out_thumbnail_catrom.png
  4. 二进制
      testdata/out_thumbnail_lanczos.png
  5. 二进制
      testdata/out_thumbnail_linear.png
  6. 二进制
      testdata/out_thumbnail_nearest.png

+ 47 - 8
resize.go

@@ -259,9 +259,8 @@ func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA
 	return Resize(img, newW, newH, filter)
 }
 
-// Fill scales the image to the smallest possible size that will cover the specified dimensions,
-// crops the resized image to the specified dimensions using the given anchor point and returns
-// the transformed image.
+// Fill creates an image with the specified dimensions and fills it with the scaled source image.
+// To achieve the correct aspect ratio without stretching, the source image will be cropped.
 //
 // Supported resample filters: NearestNeighbor, Box, Linear, Hermite, MitchellNetravali,
 // CatmullRom, BSpline, Gaussian, Lanczos, Hann, Hamming, Blackman, Bartlett, Welch, Cosine.
@@ -289,17 +288,57 @@ func Fill(img image.Image, width, height int, anchor Anchor, filter ResampleFilt
 		return Clone(img)
 	}
 
+	if srcW >= 100 && srcH >= 100 {
+		return cropAndResize(img, minW, minH, anchor, filter)
+	}
+	return resizeAndCrop(img, minW, minH, anchor, filter)
+}
+
+// cropAndResize crops the image to the smallest possible size that has the required aspect ratio using
+// the given anchor point, then scales it to the specified dimensions and returns the transformed image.
+//
+// This is generally faster than resizing first, but may result in inaccuracies when used on small source images.
+func cropAndResize(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
+	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.NRGBA
+	if srcAspectRatio < dstAspectRatio {
+		cropH := float64(srcW) * float64(dstH) / float64(dstW)
+		tmp = CropAnchor(img, srcW, int(math.Max(1, cropH)+0.5), anchor)
+	} else {
+		cropW := float64(srcH) * float64(dstW) / float64(dstH)
+		tmp = CropAnchor(img, int(math.Max(1, cropW)+0.5), srcH, anchor)
+	}
+
+	return Resize(tmp, dstW, dstH, filter)
+}
+
+// resizeAndCrop resizes the image to the smallest possible size that will cover the specified dimensions,
+// crops the resized image to the specified dimensions using the given anchor point and returns
+// the transformed image.
+func resizeAndCrop(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA {
+	dstW, dstH := width, height
+
+	srcBounds := img.Bounds()
+	srcW := srcBounds.Dx()
+	srcH := srcBounds.Dy()
 	srcAspectRatio := float64(srcW) / float64(srcH)
-	minAspectRatio := float64(minW) / float64(minH)
+	dstAspectRatio := float64(dstW) / float64(dstH)
 
 	var tmp *image.NRGBA
-	if srcAspectRatio < minAspectRatio {
-		tmp = Resize(img, minW, 0, filter)
+	if srcAspectRatio < dstAspectRatio {
+		tmp = Resize(img, dstW, 0, filter)
 	} else {
-		tmp = Resize(img, 0, minH, filter)
+		tmp = Resize(img, 0, dstH, filter)
 	}
 
-	return CropAnchor(tmp, minW, minH, anchor)
+	return CropAnchor(tmp, dstW, dstH, anchor)
 }
 
 // Thumbnail scales the image up or down using the specified resample filter, crops it

+ 63 - 0
resize_test.go

@@ -630,6 +630,24 @@ func TestThumbnail(t *testing.T) {
 	}
 }
 
+func TestThumbnailGolden(t *testing.T) {
+	for name, filter := range map[string]ResampleFilter{
+		"out_thumbnail_nearest.png": NearestNeighbor,
+		"out_thumbnail_linear.png":  Linear,
+		"out_thumbnail_catrom.png":  CatmullRom,
+		"out_thumbnail_lanczos.png": Lanczos,
+	} {
+		got := Thumbnail(testdataBranchesPNG, 150, 100, filter)
+		want, err := Open("testdata/" + name)
+		if err != nil {
+			t.Fatalf("failed to open image: %v", err)
+		}
+		if !compareNRGBA(got, toNRGBA(want), 0) {
+			t.Fatalf("resulting image differs from golden: %s", name)
+		}
+	}
+}
+
 func BenchmarkResize(b *testing.B) {
 	for _, dir := range []string{"Down", "Up"} {
 		for _, filter := range []string{"NearestNeighbor", "Linear", "CatmullRom", "Lanczos"} {
@@ -672,3 +690,48 @@ func BenchmarkResize(b *testing.B) {
 		}
 	}
 }
+
+func BenchmarkFill(b *testing.B) {
+	for _, dir := range []string{"Vertical", "Horizontal"} {
+		for _, filter := range []string{"NearestNeighbor", "Linear", "CatmullRom", "Lanczos"} {
+			for _, format := range []string{"JPEG", "PNG"} {
+				var width, height int
+				switch dir {
+				case "Vertical":
+					width = 100
+					height = 1000
+				case "Horizontal":
+					width = 1000
+					height = 100
+				}
+
+				var f ResampleFilter
+				switch filter {
+				case "NearestNeighbor":
+					f = NearestNeighbor
+				case "Linear":
+					f = Linear
+				case "CatmullRom":
+					f = CatmullRom
+				case "Lanczos":
+					f = Lanczos
+				}
+
+				var img image.Image
+				switch format {
+				case "JPEG":
+					img = testdataBranchesJPG
+				case "PNG":
+					img = testdataBranchesPNG
+				}
+
+				b.Run(fmt.Sprintf("%s %s %s", dir, filter, format), func(b *testing.B) {
+					b.ReportAllocs()
+					for i := 0; i < b.N; i++ {
+						Fill(img, width, height, Center, f)
+					}
+				})
+			}
+		}
+	}
+}

二进制
testdata/out_thumbnail_catrom.png


二进制
testdata/out_thumbnail_lanczos.png


二进制
testdata/out_thumbnail_linear.png


二进制
testdata/out_thumbnail_nearest.png