// Save returns an Image compressed using the given SaveOptions as a byte slice. func Save(image *vips.Image, options SaveOptions) ([]byte, error) { if options.Quality < 1 || options.Quality > 100 { options.Quality = DefaultQuality } if options.Compression < 1 || options.Compression > 9 { options.Compression = DefaultCompression } // Make a decision on image format and whether we're using lossless. if options.Format == Unknown { if options.AllowWebp { options.Format = Webp } else if image.HasAlpha() || useLossless(image, options) { options.Format = Png } else { options.Format = Jpeg } } else if options.Format == Webp && !useLossless(image, options) { options.Lossless = false } switch options.Format { case Jpeg: return jpegSave(image, options) case Png: return pngSave(image, options) case Webp: return webpSave(image, options) default: return nil, ErrInvalidSaveFormat } }
// DetectOrientation detects the current Image Orientation from the EXIF header. func DetectOrientation(image *vips.Image) Orientation { o, ok := image.ImageGetAsString(vips.ExifOrientation) if !ok || o == "" { return Undefined } orientation, err := strconv.Atoi(o[:1]) if err != nil || orientation <= 0 || orientation >= len(orientationInfo) { return Undefined } return Orientation(orientation) }
func crop(image *vips.Image, ow, oh int) error { m := format.MetadataImage(image) // If we have nothing to do, return. if ow == m.Width && oh == m.Height { return nil } // Center horizontally x := (m.Width - ow + 1) / 2 // Assume faces are higher up vertically y := (m.Height - oh + 1) / 4 if x < 0 || y < 0 { panic("Bad crop offsets!") } return image.ExtractArea(m.Orientation.Crop(ow, oh, x, y, m.Width, m.Height)) }
// Apply executes a set of operations to change the pixel ordering from // orientation to TopLeft. func (orientation Orientation) Apply(image *vips.Image) error { oi := &orientationInfo[orientation] if oi.apply == nil { return nil } // We want to stay sequential, so we copy memory here and execute // all work in the pipeline so far. if err := image.Write(); err != nil { return err } if err := oi.apply(image); err != nil { return err } _ = image.ImageRemove(vips.ExifOrientation) return nil }
// MetadataImage returns Metadata from an Image. Format is always unset. func MetadataImage(image *vips.Image) Metadata { o := DetectOrientation(image) w, h := o.Dimensions(image.Xsize(), image.Ysize()) if w <= 0 || h <= 0 { panic("Invalid image dimensions.") } return Metadata{Width: w, Height: h, Orientation: o, HasAlpha: image.HasAlpha()} }
func minTransparency(image *vips.Image) (float64, error) { if !image.HasAlpha() { return 1.0, nil } band, err := image.Copy() if err != nil { return 0, err } defer band.Close() if err := band.ExtractBand(band.ImageGetBands()-1, 1); err != nil { return 0, err } // If all pixels are at least 90% opaque, we can flatten. min, err := band.Min() if err != nil { return 0, err } return min / band.MaxAlpha(), nil }
func jpegSave(image *vips.Image, options SaveOptions) ([]byte, error) { // JPEG interlace saves 2-3%, but incurs a few hundred bytes of // overhead, requires buffering the image completely in RAM for // encoding and decoding, and takes over 3x the CPU. This isn't // usually beneficial on small images and is too expensive for large // images. pixels := image.Xsize() * image.Ysize() interlace := pixels >= 200*200 && pixels <= 1024*1024 // Strip and optimize both save space, enable them. return image.JpegsaveBuffer(true, options.Quality, true, interlace) }
func useLossless(image *vips.Image, options SaveOptions) bool { if !options.Lossless { return false } if !options.LossyIfPhoto { return true } // Mobile devices start being unwilling to load >= 3 megapixel PNGs. // Also we don't want to bother to edge detect on large images. if image.Xsize()*image.Ysize() >= 3*1024*1024 { return false } // Take a histogram of a Sobel edge detect of our image. What's the // highest number of histogram values in a row that are more than 1% // of the maximum value? Above 16 indicates a photo. metric, err := image.PhotoMetric(0.01) return err != nil || metric < 16 }
func pngSave(image *vips.Image, options SaveOptions) ([]byte, error) { // PNG interlace is larger; don't use it. return image.PngsaveBuffer(true, options.Compression, false) }
func transverse(image *vips.Image) error { if err := image.Flip(vips.DirectionVertical); err != nil { return err } return image.Rot(vips.Angle270) }
func rot270(image *vips.Image) error { return image.Rot(vips.Angle270) }
func rot180(image *vips.Image) error { return image.Rot(vips.Angle180) }
func rot90(image *vips.Image) error { return image.Rot(vips.Angle90) }
func flop(image *vips.Image) error { return image.Flip(vips.DirectionHorizontal) }
func flip(image *vips.Image) error { return image.Flip(vips.DirectionVertical) }
func webpSave(image *vips.Image, options SaveOptions) ([]byte, error) { return image.WebpsaveBuffer(options.Quality, options.Lossless) }
func resize(image *vips.Image, iw, ih int, fastResize bool, blurSigma float64, sharpen bool) error { m := format.MetadataImage(image) // Interpolation of RGB values with an alpha channel isn't safe // unless the values are pre-multiplied. Undo this later. // This also flattens fully transparent pixels to black. premultiply := image.HasAlpha() if premultiply { if err := image.Premultiply(); err != nil { return err } } // A box filter will quickly get us within 2x of the final size, at some quality cost. if fastResize { // Shrink factors can be passed independently here, which // may not be sane since Resize()'s blur and sharpening // steps expect a normal aspect ratio. wshrink := math.Floor(float64(m.Width) / float64(iw)) hshrink := math.Floor(float64(m.Height) / float64(ih)) if wshrink >= 2 || hshrink >= 2 { // Shrink rounds down the number of pixels. if err := image.Shrink(wshrink, hshrink); err != nil { return err } m = format.MetadataImage(image) } } // If necessary, do a high-quality resize to scale to final size. if iw < m.Width || ih < m.Height { // Vips 8.3 sometimes produces 1px smaller images than desired without the rounding help here. if err := image.Resize((float64(iw)+vips.ResizeOffset)/float64(m.Width), (float64(ih)+vips.ResizeOffset)/float64(m.Height)); err != nil { return err } } if blurSigma > 0.0 { if err := image.Gaussblur(blurSigma); err != nil { return err } } if sharpen { if err := image.MildSharpen(); err != nil { return err } } // Unpremultiply after all operations that touch adjacent pixels. if premultiply { if err := image.Unpremultiply(); err != nil { return err } } return nil }
func srgb(image *vips.Image) error { // Transform from embedded ICC profile if present or default profile // if CMYK. Ignore errors. if image.ImageFieldExists(vips.MetaIccName) { _ = image.IccTransform(sRgbFile, "", vips.IntentRelative) } else if image.ImageGuessInterpretation() == vips.InterpretationCMYK { _ = image.IccTransform(sRgbFile, cmykFile, vips.IntentRelative) } space := image.ImageGuessInterpretation() if space != vips.InterpretationSRGB && space != vips.InterpretationBW { if err := image.Colourspace(vips.InterpretationSRGB); err != nil { return err } } return nil }