diff --git a/process.go b/process.go index 42589d58..f794546a 100644 --- a/process.go +++ b/process.go @@ -37,15 +37,6 @@ func extractMeta(img *vipsImage) (int, int, int, bool) { } func calcScale(width, height int, po *processingOptions, imgtype imageType) float64 { - // If we're going only to crop, we need only to scale down to DPR. - // Scaling up while cropping is not optimal on this stage, we'll do it later if needed. - if po.Resize == resizeCrop { - if po.Dpr < 1 { - return po.Dpr - } - return 1 - } - var scale float64 srcW, srcH := float64(width), float64(height) @@ -144,14 +135,64 @@ func calcCrop(width, height, cropWidth, cropHeight int, gravity *gravityOptions) return } +func cropImage(img *vipsImage, cropWidth, cropHeight int, gravity *gravityOptions) error { + if cropWidth == 0 && cropHeight == 0 { + return nil + } + + imgWidth, imgHeight := img.Width(), img.Height() + + if cropWidth == 0 { + cropWidth = imgWidth + } else { + cropWidth = minInt(cropWidth, imgWidth) + } + + if cropHeight == 0 { + cropHeight = imgHeight + } else { + cropHeight = minInt(cropHeight, imgHeight) + } + + if cropWidth < imgWidth || cropHeight < imgHeight { + if gravity.Type == gravitySmart { + if err := img.CopyMemory(); err != nil { + return err + } + if err := img.SmartCrop(cropWidth, cropHeight); err != nil { + return err + } + // Applying additional modifications after smart crop causes SIGSEGV on Alpine + // so we have to copy memory after it + return img.CopyMemory() + } else { + left, top := calcCrop(imgWidth, imgHeight, cropWidth, cropHeight, gravity) + return img.Crop(left, top, cropWidth, cropHeight) + } + } + + return nil +} + func transformImage(ctx context.Context, img *vipsImage, data []byte, po *processingOptions, imgtype imageType) error { var err error - imgWidth, imgHeight, angle, flip := extractMeta(img) + srcWidth, srcHeight, angle, flip := extractMeta(img) - hasAlpha := img.HasAlpha() + widthToScale, heightToScale := srcWidth, srcHeight + cropWidth, cropHeight := po.Crop.Width, po.Crop.Height - scale := calcScale(imgWidth, imgHeight, po, imgtype) + if cropWidth > 0 { + widthToScale = minInt(cropWidth, srcWidth) + } + if cropHeight > 0 { + heightToScale = minInt(cropHeight, srcHeight) + } + + scale := calcScale(widthToScale, heightToScale, po, imgtype) + + cropWidth = int(float64(cropWidth) * scale) + cropHeight = int(float64(cropHeight) * scale) if scale != 1 && data != nil && canScaleOnLoad(imgtype, scale) { if imgtype == imageTypeWEBP || imgtype == imageTypeSVG { @@ -168,9 +209,13 @@ func transformImage(ctx context.Context, img *vipsImage, data []byte, po *proces } } - // Update actual image size ans scale after scale-on-load - imgWidth, imgHeight, _, _ = extractMeta(img) - scale = calcScale(imgWidth, imgHeight, po, imgtype) + // Update scale after scale-on-load + newWidth, newHeight, _, _ := extractMeta(img) + + widthToScale = int(float64(widthToScale) * float64(newWidth) / float64(srcWidth)) + heightToScale = int(float64(heightToScale) * float64(newHeight) / float64(srcHeight)) + + scale = calcScale(widthToScale, heightToScale, po, imgtype) } if err = img.Rad2Float(); err != nil { @@ -189,15 +234,14 @@ func transformImage(ctx context.Context, img *vipsImage, data []byte, po *proces } } + hasAlpha := img.HasAlpha() + if scale != 1 { if err = img.Resize(scale, hasAlpha); err != nil { return err } } - // Update actual image size after resize - imgWidth, imgHeight, _, _ = extractMeta(img) - checkTimeout(ctx) if angle != vipsAngleD0 || flip { @@ -220,58 +264,41 @@ func transformImage(ctx context.Context, img *vipsImage, data []byte, po *proces checkTimeout(ctx) - cropW, cropH := po.Width, po.Height + dprWidth := int(float64(po.Width) * po.Dpr) + dprHeight := int(float64(po.Height) * po.Dpr) - if po.Dpr < 1 || (po.Dpr > 1 && po.Resize != resizeCrop) { - cropW = int(float64(cropW) * po.Dpr) - cropH = int(float64(cropH) * po.Dpr) + cropGravity := po.Crop.Gravity + if cropGravity.Type == gravityUnknown { + cropGravity = po.Gravity } - if cropW == 0 { - cropW = imgWidth - } else { - cropW = minInt(cropW, imgWidth) - } - - if cropH == 0 { - cropH = imgHeight - } else { - cropH = minInt(cropH, imgHeight) - } - - if cropW < imgWidth || cropH < imgHeight { - if po.Gravity.Type == gravitySmart { - if err = img.CopyMemory(); err != nil { - return err - } - if err = img.SmartCrop(cropW, cropH); err != nil { - return err - } - // Applying additional modifications after smart crop causes SIGSEGV on Alpine - // so we have to copy memory after it - if err = img.CopyMemory(); err != nil { - return err - } - } else { - left, top := calcCrop(imgWidth, imgHeight, cropW, cropH, &po.Gravity) - if err = img.Crop(left, top, cropW, cropH); err != nil { - return err - } + if cropGravity.Type == po.Gravity.Type && cropGravity.Type != gravityFocusPoint { + if cropWidth == 0 { + cropWidth = dprWidth + } else if dprWidth > 0 { + cropWidth = minInt(cropWidth, dprWidth) } - checkTimeout(ctx) - } + if cropHeight == 0 { + cropHeight = dprHeight + } else if dprHeight > 0 { + cropHeight = minInt(cropHeight, dprHeight) + } - if po.Enlarge && po.Resize == resizeCrop && po.Dpr > 1 { - // We didn't enlarge the image before, because is wasn't optimal. Now it's time to do it - if err = img.Resize(po.Dpr, hasAlpha); err != nil { + if err = cropImage(img, cropWidth, cropHeight, &cropGravity); err != nil { return err } - if err = img.CopyMemory(); err != nil { + } else { + if err = cropImage(img, cropWidth, cropHeight, &cropGravity); err != nil { + return err + } + if err = cropImage(img, dprWidth, dprHeight, &po.Gravity); err != nil { return err } } + checkTimeout(ctx) + if convertToLinear { if err = img.FixColourspace(); err != nil { return err @@ -444,6 +471,15 @@ func processImage(ctx context.Context) ([]byte, context.CancelFunc, error) { } } + if po.Resize == resizeCrop { + logWarning("`crop` resizing type is deprecated and will be removed in future versions. Use `crop` processing option instead") + + po.Crop.Width, po.Crop.Height = po.Width, po.Height + + po.Resize = resizeFit + po.Width, po.Height = 0, 0 + } + animationSupport := conf.MaxGifFrames > 1 && vipsSupportAnimation(imgtype) && vipsSupportAnimation(po.Format) pages := 1 diff --git a/processing_options.go b/processing_options.go index 9b9e84aa..b7208f17 100644 --- a/processing_options.go +++ b/processing_options.go @@ -24,7 +24,8 @@ type processingHeaders struct { type gravityType int const ( - gravityCenter gravityType = iota + gravityUnknown gravityType = iota + gravityCenter gravityNorth gravityEast gravitySouth @@ -79,6 +80,12 @@ const ( hexColorShortFormat = "%1x%1x%1x" ) +type cropOptions struct { + Width int + Height int + Gravity gravityOptions +} + type watermarkOptions struct { Enabled bool Opacity float64 @@ -97,6 +104,7 @@ type processingOptions struct { Gravity gravityOptions Enlarge bool Expand bool + Crop cropOptions Format imageType Quality int Flatten bool @@ -240,18 +248,52 @@ func decodeURL(parts []string) (string, string, error) { return decodeBase64URL(parts) } +func parseDimension(d *int, name, arg string) error { + if v, err := strconv.Atoi(arg); err == nil && v >= 0 { + *d = v + } else { + return fmt.Errorf("Invalid %s: %s", name, arg) + } + + return nil +} + +func parseGravity(g *gravityOptions, args []string) error { + if t, ok := gravityTypes[args[0]]; ok { + g.Type = t + } else { + return fmt.Errorf("Invalid gravity: %s", args[0]) + } + + if g.Type == gravityFocusPoint { + if len(args) != 3 { + return fmt.Errorf("Invalid gravity arguments: %v", args) + } + + if x, err := strconv.ParseFloat(args[1], 64); err == nil && x >= 0 && x <= 1 { + g.X = x + } else { + return fmt.Errorf("Invalid gravity X: %s", args[1]) + } + + if y, err := strconv.ParseFloat(args[2], 64); err == nil && y >= 0 && y <= 1 { + g.Y = y + } else { + return fmt.Errorf("Invalid gravity Y: %s", args[2]) + } + } else if len(args) > 1 { + return fmt.Errorf("Invalid gravity arguments: %v", args) + } + + return nil +} + func applyWidthOption(po *processingOptions, args []string) error { if len(args) > 1 { return fmt.Errorf("Invalid width arguments: %v", args) } - if w, err := strconv.Atoi(args[0]); err == nil && w >= 0 { - po.Width = w - } else { - return fmt.Errorf("Invalid width: %s", args[0]) - } - - return nil + return parseDimension(&po.Width, "width", args[0]) } func applyHeightOption(po *processingOptions, args []string) error { @@ -259,13 +301,7 @@ func applyHeightOption(po *processingOptions, args []string) error { return fmt.Errorf("Invalid height arguments: %v", args) } - if h, err := strconv.Atoi(args[0]); err == nil && h >= 0 { - po.Height = h - } else { - return fmt.Errorf("Invalid height: %s", args[0]) - } - - return nil + return parseDimension(&po.Height, "height", args[0]) } func applyEnlargeOption(po *processingOptions, args []string) error { @@ -369,30 +405,26 @@ func applyDprOption(po *processingOptions, args []string) error { } func applyGravityOption(po *processingOptions, args []string) error { - if g, ok := gravityTypes[args[0]]; ok { - po.Gravity.Type = g - } else { - return fmt.Errorf("Invalid gravity: %s", args[0]) + return parseGravity(&po.Gravity, args) +} + +func applyCropOption(po *processingOptions, args []string) error { + if len(args) > 5 { + return fmt.Errorf("Invalid crop arguments: %v", args) } - if po.Gravity.Type == gravityFocusPoint { - if len(args) != 3 { - return fmt.Errorf("Invalid gravity arguments: %v", args) - } + if err := parseDimension(&po.Crop.Width, "crop width", args[0]); err != nil { + return err + } - if x, err := strconv.ParseFloat(args[1], 64); err == nil && x >= 0 && x <= 1 { - po.Gravity.X = x - } else { - return fmt.Errorf("Invalid gravity X: %s", args[1]) + if len(args) > 1 { + if err := parseDimension(&po.Crop.Height, "crop height", args[1]); err != nil { + return err } + } - if y, err := strconv.ParseFloat(args[2], 64); err == nil && y >= 0 && y <= 1 { - po.Gravity.Y = y - } else { - return fmt.Errorf("Invalid gravity Y: %s", args[2]) - } - } else if len(args) > 1 { - return fmt.Errorf("Invalid gravity arguments: %v", args) + if len(args) > 2 { + return parseGravity(&po.Crop.Gravity, args[2:]) } return nil @@ -625,6 +657,10 @@ func applyProcessingOption(po *processingOptions, name string, args []string) er if err := applyGravityOption(po, args); err != nil { return err } + case "crop", "c": + if err := applyCropOption(po, args); err != nil { + return err + } case "quality", "q": if err := applyQualityOption(po, args); err != nil { return err