Crop the centre before resizing for fit=cover (closes #282) #284
@@ -5,6 +5,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"math"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -139,3 +140,129 @@ func TestImageProcessor_FitCoverCropsTheCentre(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The greys of createSquarePNG, and the grey halfway between them.
|
||||
const (
|
||||
backgroundGrey = 192
|
||||
squareGrey = 64
|
||||
halfwayGrey = (backgroundGrey + squareGrey) / 2
|
||||
)
|
||||
|
||||
// createSquarePNG creates a grey PNG side pixels square with a darker grey
|
||||
// square squareSide pixels wide in its centre.
|
||||
func createSquarePNG(t *testing.T, side, squareSide int) []byte {
|
||||
t.Helper()
|
||||
|
||||
first := (side - squareSide) / 2
|
||||
last := first + squareSide - 1
|
||||
|
||||
img := image.NewGray(image.Rect(0, 0, side, side))
|
||||
for y := range side {
|
||||
for x := range side {
|
||||
grey := uint8(backgroundGrey)
|
||||
if x >= first && x <= last && y >= first && y <= last {
|
||||
grey = squareGrey
|
||||
}
|
||||
|
||||
img.SetGray(x, y, color.Gray{Y: grey})
|
||||
}
|
||||
}
|
||||
|
||||
return encodePNG(t, img)
|
||||
}
|
||||
|
||||
// squareLength returns how long the square is along greys, the greys of a
|
||||
// row or column of the output that crosses it: the distance between the
|
||||
// first and the last point where greys passes halfwayGrey, or 0 when there
|
||||
// is no such point. Resampling blurs the square's edges over several
|
||||
// pixels, and each point is placed between the two pixels on either side
|
||||
// of it in proportion to their greys, so the length is measured to a
|
||||
// fraction of a pixel.
|
||||
func squareLength(greys []float64) float64 {
|
||||
first, last := -1.0, -1.0
|
||||
|
||||
for i := 1; i < len(greys); i++ {
|
||||
before, after := greys[i-1], greys[i]
|
||||
if (before > halfwayGrey) == (after > halfwayGrey) {
|
||||
continue
|
||||
}
|
||||
|
||||
point := float64(i-1) + (before-halfwayGrey)/(before-after)
|
||||
if first < 0 {
|
||||
first = point
|
||||
}
|
||||
|
||||
last = point
|
||||
}
|
||||
|
||||
return last - first
|
||||
}
|
||||
|
||||
// TestImageProcessor_FitCoverKeepsProportions verifies that FitCover scales
|
||||
// both sides of the source by the same amount: a square in the centre of
|
||||
// the source comes out square. Its width and height are measured along the
|
||||
// output's middle row and column.
|
||||
func TestImageProcessor_FitCoverKeepsProportions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
side, squareSide int
|
||||
size Size
|
||||
}{
|
||||
{"16x16 at 100x60", 16, 8, Size{Width: 100, Height: 60}},
|
||||
{"16x16 at 60x100", 16, 8, Size{Width: 60, Height: 100}},
|
||||
{"10x10 at 100x35", 10, 2, Size{Width: 100, Height: 35}},
|
||||
{"10x10 at 35x100", 10, 2, Size{Width: 35, Height: 100}},
|
||||
{"64x64 at 300x100", 64, 16, Size{Width: 300, Height: 100}},
|
||||
{"64x64 at 100x300", 64, 16, Size{Width: 100, Height: 300}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
input := createSquarePNG(t, tt.side, tt.squareSide)
|
||||
|
||||
result, err := New(Params{}).Process(t.Context(),
|
||||
bytes.NewReader(input),
|
||||
&Request{Size: tt.size, Format: FormatPNG, FitMode: FitCover})
|
||||
if err != nil {
|
||||
t.Fatalf("Process() error = %v", err)
|
||||
}
|
||||
|
||||
defer func() { _ = result.Content.Close() }()
|
||||
|
||||
output, err := png.Decode(result.Content)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to decode output: %v", err)
|
||||
}
|
||||
|
||||
bounds := output.Bounds()
|
||||
if bounds.Dx() != tt.size.Width || bounds.Dy() != tt.size.Height {
|
||||
t.Fatalf("Process() made %dx%d, want %dx%d",
|
||||
bounds.Dx(), bounds.Dy(), tt.size.Width, tt.size.Height)
|
||||
}
|
||||
|
||||
row := make([]float64, bounds.Dx())
|
||||
for x := range row {
|
||||
r, _, _, _ := output.At(x, bounds.Dy()/2).RGBA()
|
||||
row[x] = float64(r >> 8)
|
||||
}
|
||||
|
||||
column := make([]float64, bounds.Dy())
|
||||
for y := range column {
|
||||
r, _, _, _ := output.At(bounds.Dx()/2, y).RGBA()
|
||||
column[y] = float64(r >> 8)
|
||||
}
|
||||
|
||||
width, height := squareLength(row), squareLength(column)
|
||||
t.Logf("square is %.2f wide and %.2f high", width, height)
|
||||
|
||||
if width == 0 || math.Abs(width-height) > 0.5 {
|
||||
t.Errorf("square is %.2f wide and %.2f high, want it square",
|
||||
width, height)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user