Explorar el Código

Add `flip` processing option

DarthSim hace 3 meses
padre
commit
05e8dd7e12
Se han modificado 8 ficheros con 131 adiciones y 13 borrados
  1. 6 0
      CHANGELOG.md
  2. 36 4
      options/gravity_options.go
  3. 24 0
      options/processing_options.go
  4. 14 2
      processing/crop.go
  5. 16 6
      processing/rotate_and_flip.go
  6. 6 0
      vips/vips.c
  7. 28 1
      vips/vips.go
  8. 1 0
      vips/vips.h

+ 6 - 0
CHANGELOG.md

@@ -1,10 +1,16 @@
 # Changelog
 
 ## [Unreleased]
+### Added
+- Add [flip](https://docs.imgproxy.net/latest/usage/processing#flip) processing option.
+
 ### Changed
 - When image source responds with a 4xx status code, imgproxy now responds with the same status code instead of always responding with `404 Not Found`.
 - When image source responds with a 5xx status code, imgproxy now responds with `502 Bad Gateway` instead of `500 Internal Server Error`.
 
+### Fixed
+- Fix crop coordinates calculation when the image has an EXIF orientation different from `1` and the `rotate` processing option is used.
+
 ## [3.30.1] - 2025-10-10
 ### Changed
 - Format New Relic and OpenTelemetry metadata values that implement the `fmt.Stringer` interface as strings.

+ 36 - 4
options/gravity_options.go

@@ -106,7 +106,7 @@ var gravityTypesRotationMap = map[int]map[GravityType]GravityType{
 	},
 }
 
-var gravityTypesFlipMap = map[GravityType]GravityType{
+var gravityTypesFlipXMap = map[GravityType]GravityType{
 	GravityEast:      GravityWest,
 	GravityWest:      GravityEast,
 	GravityNorthWest: GravityNorthEast,
@@ -115,6 +115,15 @@ var gravityTypesFlipMap = map[GravityType]GravityType{
 	GravitySouthEast: GravitySouthWest,
 }
 
+var gravityTypesFlipYMap = map[GravityType]GravityType{
+	GravityNorth:     GravitySouth,
+	GravitySouth:     GravityNorth,
+	GravityNorthWest: GravitySouthWest,
+	GravityNorthEast: GravitySouthEast,
+	GravitySouthWest: GravityNorthWest,
+	GravitySouthEast: GravityNorthEast,
+}
+
 func (gt GravityType) String() string {
 	for k, v := range gravityTypes {
 		if v == gt {
@@ -138,11 +147,21 @@ type GravityOptions struct {
 	X, Y float64
 }
 
-func (g *GravityOptions) RotateAndFlip(angle int, flip bool) {
+// RotateAndFlip rotates and flips the gravity options so that they correspond
+// to the image before rotation and flipping.
+//
+// By design, rotation and flipping are applied before cropping.
+// But for performance reasons, we do cropping before rotation and flipping.
+// So we need to adjust gravity options accordingly.
+// As a result, cropping with the adjusted gravity options
+// and then rotating/flipping the cropped image gives the same result
+// as rotating/flipping the image first and then cropping
+// with the original gravity options.
+func (g *GravityOptions) RotateAndFlip(angle int, flipX, flipY bool) {
 	angle %= 360
 
-	if flip {
-		if gt, ok := gravityTypesFlipMap[g.Type]; ok {
+	if flipX {
+		if gt, ok := gravityTypesFlipXMap[g.Type]; ok {
 			g.Type = gt
 		}
 
@@ -154,6 +173,19 @@ func (g *GravityOptions) RotateAndFlip(angle int, flip bool) {
 		}
 	}
 
+	if flipY {
+		if gt, ok := gravityTypesFlipYMap[g.Type]; ok {
+			g.Type = gt
+		}
+
+		switch g.Type {
+		case GravityCenter, GravityEast, GravityWest:
+			g.Y = -g.Y
+		case GravityFocusPoint:
+			g.Y = 1.0 - g.Y
+		}
+	}
+
 	if angle > 0 {
 		if rotMap := gravityTypesRotationMap[angle]; rotMap != nil {
 			if gt, ok := rotMap[g.Type]; ok {

+ 24 - 0
options/processing_options.go

@@ -60,6 +60,11 @@ func (wo WatermarkOptions) ShouldReplicate() bool {
 	return wo.Position.Type == GravityReplicate
 }
 
+type FlipOptions struct {
+	Horizontal bool
+	Vertical   bool
+}
+
 type ProcessingOptions struct {
 	ResizingType      ResizeType
 	Width             int
@@ -77,6 +82,7 @@ type ProcessingOptions struct {
 	Padding           PaddingOptions
 	Trim              TrimOptions
 	Rotate            int
+	Flip              FlipOptions
 	Format            imagetype.Type
 	Quality           int
 	FormatQuality     map[imagetype.Type]int
@@ -133,6 +139,7 @@ func NewProcessingOptions() *ProcessingOptions {
 		Padding:           PaddingOptions{Enabled: false},
 		Trim:              TrimOptions{Enabled: false, Threshold: 10, Smart: true},
 		Rotate:            0,
+		Flip:              FlipOptions{Horizontal: false, Vertical: false},
 		Quality:           0,
 		MaxBytes:          0,
 		Format:            imagetype.Unknown,
@@ -549,6 +556,21 @@ func applyRotateOption(po *ProcessingOptions, args []string) error {
 	return nil
 }
 
+func applyFlipOption(po *ProcessingOptions, args []string) error {
+	if len(args) > 2 {
+		return newOptionArgumentError("Invalid flip arguments: %v", args)
+	}
+
+	if len(args[0]) > 0 {
+		po.Flip.Horizontal = parseBoolOption(args[0])
+	}
+	if len(args) > 1 && len(args[1]) > 0 {
+		po.Flip.Vertical = parseBoolOption(args[1])
+	}
+
+	return nil
+}
+
 func applyQualityOption(po *ProcessingOptions, args []string) error {
 	if len(args) > 1 {
 		return newOptionArgumentError("Invalid quality arguments: %v", args)
@@ -1022,6 +1044,8 @@ func applyURLOption(po *ProcessingOptions, name string, args []string, usedPrese
 		return applyAutoRotateOption(po, args)
 	case "rotate", "rot":
 		return applyRotateOption(po, args)
+	case "flip", "fl":
+		return applyFlipOption(po, args)
 	case "background", "bg":
 		return applyBackgroundOption(po, args)
 	case "blur", "bl":

+ 14 - 2
processing/crop.go

@@ -35,10 +35,22 @@ func cropImage(img *vips.Image, cropWidth, cropHeight int, gravity *options.Grav
 func crop(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
 	width, height := pctx.cropWidth, pctx.cropHeight
 
+	// Since we crop before rotating and flipping,
+	// we need to adjust gravity options accordingly.
+	// After rotation and flipping, we'll get the same result
+	// as if we cropped with the original gravity options after
+	// rotation and flipping.
+	//
+	// During rotation/flipping, we first apply the EXIF orientation,
+	// then the user-specified operations.
+	// So here we apply the adjustments in the reverse order.
 	opts := pctx.cropGravity
-	opts.RotateAndFlip(pctx.angle, pctx.flip)
-	opts.RotateAndFlip(po.Rotate, false)
+	opts.RotateAndFlip(po.Rotate, po.Flip.Horizontal, po.Flip.Vertical)
+	opts.RotateAndFlip(pctx.angle, pctx.flip, false)
 
+	// If the final image is rotated by 90 or 270 degrees,
+	// we need to swap width and height for cropping.
+	// After rotation, we'll get the originally intended dimensions.
 	if (pctx.angle+po.Rotate)%180 == 90 {
 		width, height = height, width
 	}

+ 16 - 6
processing/rotate_and_flip.go

@@ -7,23 +7,33 @@ import (
 )
 
 func rotateAndFlip(pctx *pipelineContext, img *vips.Image, po *options.ProcessingOptions, imgdata *imagedata.ImageData) error {
-	if pctx.angle%360 == 0 && po.Rotate%360 == 0 && !pctx.flip {
+	shouldRotate := pctx.angle%360 != 0 || po.Rotate%360 != 0
+	shouldFlip := pctx.flip || po.Flip.Horizontal || po.Flip.Vertical
+
+	if !shouldRotate && !shouldFlip {
 		return nil
 	}
 
+	// We need the image in random access mode, so we copy it to memory.
 	if err := img.CopyMemory(); err != nil {
 		return err
 	}
 
+	// Rotate according to EXIF orientation
 	if err := img.Rotate(pctx.angle); err != nil {
 		return err
 	}
 
-	if pctx.flip {
-		if err := img.Flip(); err != nil {
-			return err
-		}
+	// Flip according to EXIF orientation
+	if err := img.Flip(pctx.flip, false); err != nil {
+		return err
+	}
+
+	// Rotate according to user-specified options
+	if err := img.Rotate(po.Rotate); err != nil {
+		return err
 	}
 
-	return img.Rotate(po.Rotate)
+	// Flip according to user-specified options
+	return img.Flip(po.Flip.Horizontal, po.Flip.Vertical)
 }

+ 6 - 0
vips/vips.c

@@ -576,6 +576,12 @@ vips_flip_horizontal_go(VipsImage *in, VipsImage **out)
   return vips_flip(in, out, VIPS_DIRECTION_HORIZONTAL, NULL);
 }
 
+int
+vips_flip_vertical_go(VipsImage *in, VipsImage **out)
+{
+  return vips_flip(in, out, VIPS_DIRECTION_VERTICAL, NULL);
+}
+
 int
 vips_smartcrop_go(VipsImage *in, VipsImage **out, int width, int height)
 {

+ 28 - 1
vips/vips.go

@@ -685,7 +685,7 @@ func (img *Image) Rotate(angle int) error {
 	return nil
 }
 
-func (img *Image) Flip() error {
+func (img *Image) FlipHorizontal() error {
 	var tmp *C.VipsImage
 
 	if C.vips_flip_horizontal_go(img.VipsImage, &tmp) != 0 {
@@ -696,6 +696,33 @@ func (img *Image) Flip() error {
 	return nil
 }
 
+func (img *Image) FlipVertical() error {
+	var tmp *C.VipsImage
+
+	if C.vips_flip_vertical_go(img.VipsImage, &tmp) != 0 {
+		return Error()
+	}
+
+	img.swapAndUnref(tmp)
+	return nil
+}
+
+func (img *Image) Flip(horizontal, vertical bool) error {
+	if horizontal {
+		if err := img.FlipHorizontal(); err != nil {
+			return err
+		}
+	}
+
+	if vertical {
+		if err := img.FlipVertical(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (img *Image) Crop(left, top, width, height int) error {
 	var tmp *C.VipsImage
 

+ 1 - 0
vips/vips.h

@@ -63,6 +63,7 @@ int vips_colourspace_go(VipsImage *in, VipsImage **out, VipsInterpretation cs);
 
 int vips_rot_go(VipsImage *in, VipsImage **out, VipsAngle angle);
 int vips_flip_horizontal_go(VipsImage *in, VipsImage **out);
+int vips_flip_vertical_go(VipsImage *in, VipsImage **out);
 
 int vips_extract_area_go(VipsImage *in, VipsImage **out, int left, int top, int width, int height);
 int vips_smartcrop_go(VipsImage *in, VipsImage **out, int width, int height);