Skip to content

Commit

Permalink
Animated GIF support (#246)
Browse files Browse the repository at this point in the history
* - add golden

* - load animated GIF
- export GIF
- resize animated images
- export animated images

* - add Pages to ImageMetadata
- when building ImageMetadata add all the props

* - Support rotation of animated images
- Add imageRef.Grid() func
- rename ...pages... functions to something more reasonable

* - add golden

* - keep frame (page) order when rotating 270

* - Return error for unsupported operations on multi-page images
- fix 270 rotation of multi-page images
- Pages() instead of GetPages() is more Go like
- Orientation() instead of GetOrientation() is more Go like
  • Loading branch information
Elad Laufer committed Jan 15, 2022
1 parent 423eb69 commit 623c60d
Show file tree
Hide file tree
Showing 17 changed files with 350 additions and 57 deletions.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
4 changes: 4 additions & 0 deletions vips/conversion.c
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,7 @@ int arrayjoin(VipsImage **in, VipsImage **out, int n, int across) {
int replicate(VipsImage *in, VipsImage **out, int across, int down) {
return vips_replicate(in, out, across, down, NULL);
}

int grid(VipsImage *in, VipsImage **out, int tileHeight, int across, int down){
return vips_grid(in, out, tileHeight, across, down, NULL);
}
11 changes: 11 additions & 0 deletions vips/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,3 +416,14 @@ func vipsReplicate(in *C.VipsImage, across int, down int) (*C.VipsImage, error)
}
return out, nil
}

// https://www.libvips.org/API/current/libvips-conversion.html#vips-grid
func vipsGrid(in *C.VipsImage, tileHeight, across, down int) (*C.VipsImage, error) {
incOpCounter("grid")
var out *C.VipsImage

if err := C.grid(in, &out, C.int(tileHeight), C.int(across), C.int(down)); err != 0 {
return nil, handleImageError(out)
}
return out, nil
}
2 changes: 2 additions & 0 deletions vips/conversion.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ int arrayjoin(VipsImage **in, VipsImage **out, int n, int across);
int is_16bit(VipsInterpretation interpretation);

int replicate(VipsImage *in, VipsImage **out, int across, int down);

int grid(VipsImage *in, VipsImage **out, int tileHeight, int across, int down);
3 changes: 2 additions & 1 deletion vips/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (

var (
// ErrUnsupportedImageFormat when image type is unsupported
ErrUnsupportedImageFormat = errors.New("unsupported image format")
ErrUnsupportedImageFormat = errors.New("unsupported image format")
ErrUnsupportedMultiPageOperation = errors.New("unsupported operation for multi-page image")
)

func handleImageError(out *C.VipsImage) error {
Expand Down
25 changes: 20 additions & 5 deletions vips/header.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,26 @@ void set_meta_orientation(VipsImage *in, int orientation) {
vips_image_set_int(in, VIPS_META_ORIENTATION, orientation);
}

//https://libvips.github.io/libvips/API/current/libvips-header.html#vips-image-get-n-pages
int get_image_get_n_pages(VipsImage *in) {
int page = 0;
page = vips_image_get_n_pages(in);
return page;
// https://libvips.github.io/libvips/API/current/libvips-header.html#vips-image-get-n-pages
int get_image_n_pages(VipsImage *in) {
int n_pages = 0;
n_pages = vips_image_get_n_pages(in);
return n_pages;
}

void set_image_n_pages(VipsImage *in, int n_pages) {
vips_image_set_int(in, VIPS_META_N_PAGES, n_pages);
}

// https://www.libvips.org/API/current/libvips-header.html#vips-image-get-page-height
int get_page_height(VipsImage *in) {
int page_height = 0;
page_height = vips_image_get_page_height(in);
return page_height;
}

void set_page_height(VipsImage *in, int height) {
vips_image_set_int(in, VIPS_META_PAGE_HEIGHT, height);
}

int get_meta_loader(const VipsImage *in, const char **out) {
Expand Down
16 changes: 14 additions & 2 deletions vips/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,20 @@ func vipsSetMetaOrientation(in *C.VipsImage, orientation int) {
C.set_meta_orientation(in, C.int(orientation))
}

func vipsImageGetPages(in *C.VipsImage) int {
return int(C.get_image_get_n_pages(in))
func vipsGetImageNPages(in *C.VipsImage) int {
return int(C.get_image_n_pages(in))
}

func vipsSetImageNPages(in *C.VipsImage, pages int) {
C.set_image_n_pages(in, C.int(pages))
}

func vipsGetPageHeight(in *C.VipsImage) int {
return int(C.get_page_height(in))
}

func vipsSetPageHeight(in *C.VipsImage, height int) {
C.set_page_height(in, C.int(height))
}

func vipsImageGetMetaLoader(in *C.VipsImage) (string, bool) {
Expand Down
5 changes: 4 additions & 1 deletion vips/header.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ void remove_metadata(VipsImage *in);
int get_meta_orientation(VipsImage *in);
void remove_meta_orientation(VipsImage *in);
void set_meta_orientation(VipsImage *in, int orientation);
int get_image_get_n_pages(VipsImage *in);
int get_image_n_pages(VipsImage *in);
void set_image_n_pages(VipsImage *in, int n_pages);
int get_page_height(VipsImage *in);
void set_page_height(VipsImage *in, int height);
int get_meta_loader(const VipsImage *in, const char **out);
166 changes: 145 additions & 21 deletions vips/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type ImageMetadata struct {
Height int
Colorspace Interpretation
Orientation int
Pages int
}

type Parameter struct {
Expand Down Expand Up @@ -339,13 +340,18 @@ func NewImageFromReader(r io.Reader) (*ImageRef, error) {

// NewImageFromFile loads an image from file and creates a new ImageRef
func NewImageFromFile(file string) (*ImageRef, error) {
return LoadImageFromFile(file, nil)
}

// LoadImageFromFile loads an image from file and creates a new ImageRef
func LoadImageFromFile(file string, params *ImportParams) (*ImageRef, error) {
buf, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}

govipsLog("govips", LogLevelDebug, fmt.Sprintf("creating imageref from file %s", file))
return NewImageFromBuffer(buf)
govipsLog("govips", LogLevelDebug, fmt.Sprintf("creating imageRef from file %s", file))
return LoadImageFromBuffer(buf, params)
}

// NewImageFromBuffer loads an image buffer and creates a new Image
Expand All @@ -368,7 +374,7 @@ func LoadImageFromBuffer(buf []byte, params *ImportParams) (*ImageRef, error) {

ref := newImageRef(vipsImage, format, buf)

govipsLog("govips", LogLevelDebug, fmt.Sprintf("created imageref %p", ref))
govipsLog("govips", LogLevelDebug, fmt.Sprintf("created imageRef %p", ref))
return ref, nil
}

Expand Down Expand Up @@ -435,9 +441,12 @@ func NewThumbnailWithSizeFromBuffer(buf []byte, width, height int, crop Interest
// Metadata returns the metadata (ImageMetadata struct) of the associated ImageRef
func (r *ImageRef) Metadata() *ImageMetadata {
return &ImageMetadata{
Format: r.Format(),
Width: r.Width(),
Height: r.Height(),
Format: r.Format(),
Width: r.Width(),
Height: r.Height(),
Orientation: r.Orientation(),
Colorspace: r.ColorSpace(),
Pages: r.Pages(),
}
}

Expand Down Expand Up @@ -542,11 +551,16 @@ func (r *ImageRef) HasAlpha() bool {
return vipsHasAlpha(r.image)
}

// GetOrientation returns the orientation number as it appears in the EXIF, if present
func (r *ImageRef) GetOrientation() int {
// Orientation returns the orientation number as it appears in the EXIF, if present
func (r *ImageRef) Orientation() int {
return vipsGetMetaOrientation(r.image)
}

// Deprecated: use Orientation() instead
func (r *ImageRef) GetOrientation() int {
return r.Orientation()
}

// SetOrientation sets the orientation in the EXIF header of the associated image.
func (r *ImageRef) SetOrientation(orientation int) error {
out, err := vipsCopyImage(r.image)
Expand Down Expand Up @@ -618,19 +632,54 @@ func (r *ImageRef) IsColorSpaceSupported() bool {
return vipsIsColorSpaceSupported(r.image)
}

func (r *ImageRef) newMetadata(format ImageType) *ImageMetadata {
return &ImageMetadata{
Format: format,
Width: r.Width(),
Height: r.Height(),
Colorspace: r.ColorSpace(),
Orientation: r.GetOrientation(),
}
// Pages returns the number of pages in the Image
// For animated images this corresponds to the number of frames
func (r *ImageRef) Pages() int {
return vipsGetImageNPages(r.image)
}

// GetPages returns the number of Image
// Deprecated: use Pages() instead
func (r *ImageRef) GetPages() int {
return vipsImageGetPages(r.image)
return r.Pages()
}

// SetPages sets the number of pages in the Image
// For animated images this corresponds to the number of frames
func (r *ImageRef) SetPages(pages int) error {
out, err := vipsCopyImage(r.image)
if err != nil {
return err
}

vipsSetImageNPages(r.image, pages)

r.setImage(out)
return nil
}

// PageHeight return the height of a single page
func (r *ImageRef) PageHeight() int {
return vipsGetPageHeight(r.image)
}

// GetPageHeight return the height of a single page
// Deprecated use PageHeight() instead
func (r *ImageRef) GetPageHeight() int {
return vipsGetPageHeight(r.image)
}

// SetPageHeight set the height of a page
// For animated images this is used when "unrolling" back to frames
func (r *ImageRef) SetPageHeight(height int) error {
out, err := vipsCopyImage(r.image)
if err != nil {
return err
}

vipsSetPageHeight(out, height)

r.setImage(out)
return nil
}

// Export creates a byte array of the image for use.
Expand Down Expand Up @@ -722,6 +771,8 @@ func (r *ImageRef) ExportNative() ([]byte, *ImageMetadata, error) {
return r.ExportAvif(NewAvifExportParams())
case ImageTypeJP2K:
return r.ExportJp2k(NewJp2kExportParams())
case ImageTypeGIF:
return r.ExportGIF(NewGifExportParams())
default:
return r.ExportJpeg(NewJpegExportParams())
}
Expand Down Expand Up @@ -1099,6 +1150,7 @@ func GetRotationAngleFromExif(orientation int) (Angle, bool) {
// N.B. libvips does not flip images currently (i.e. no support for orientations 2, 4, 5 and 7).
// N.B. due to the HEIF image standard, HEIF images are always autorotated by default on load.
// Thus, calling AutoRotate for HEIF images is not needed.
// todo: use https://www.libvips.org/API/current/libvips-conversion.html#vips-autorot-remove-angle
func (r *ImageRef) AutoRotate() error {
out, err := vipsAutoRotate(r.image)
if err != nil {
Expand All @@ -1110,6 +1162,10 @@ func (r *ImageRef) AutoRotate() error {

// ExtractArea crops the image to a specified area
func (r *ImageRef) ExtractArea(left, top, width, height int) error {
if err := r.multiPageNotSupported(); err != nil {
return err
}

out, err := vipsExtractArea(r.image, left, top, width, height)
if err != nil {
return err
Expand All @@ -1134,7 +1190,6 @@ func (r *ImageRef) RemoveICCProfile() error {

// TransformICCProfile transforms from the embedded ICC profile of the image to the icc profile at the given path.
func (r *ImageRef) TransformICCProfile(outputProfilePath string) error {

// If the image has an embedded profile, that will be used and the input profile ignored.
// Otherwise, images without an input profile are assumed to use a standard RGB profile.
embedded := r.HasICCProfile()
Expand Down Expand Up @@ -1393,17 +1448,30 @@ func (r *ImageRef) Resize(scale float64, kernel Kernel) error {
// ResizeWithVScale resizes the image with both horizontal and vertical scaling.
// The parameters are the scaling factors.
func (r *ImageRef) ResizeWithVScale(hScale, vScale float64, kernel Kernel) error {
err := r.PremultiplyAlpha()
if err != nil {
if err := r.PremultiplyAlpha(); err != nil {
return err
}

pages := r.Pages()
pageHeight := r.GetPageHeight()

out, err := vipsResizeWithVScale(r.image, hScale, vScale, kernel)
if err != nil {
return err
}
r.setImage(out)

if pages > 1 {
scale := hScale
if vScale != -1 {
scale = vScale
}
newPageHeight := int(float64(pageHeight) * scale)
if err := r.SetPageHeight(newPageHeight); err != nil {
return err
}
}

return r.UnpremultiplyAlpha()
}

Expand Down Expand Up @@ -1472,11 +1540,38 @@ func (r *ImageRef) Flip(direction Direction) error {

// Rotate rotates the image by multiples of 90 degrees. To rotate by arbitrary angles use Similarity.
func (r *ImageRef) Rotate(angle Angle) error {
width := r.Width()

if r.Pages() > 1 && (angle == Angle90 || angle == Angle270) {
if angle == Angle270 {
if err := r.Flip(DirectionHorizontal); err != nil {
return err
}
}

if err := r.Grid(r.GetPageHeight(), r.Pages(), 1); err != nil {
return err
}

if angle == Angle270 {
if err := r.Flip(DirectionHorizontal); err != nil {
return err
}
}

}

out, err := vipsRotate(r.image, angle)
if err != nil {
return err
}
r.setImage(out)

if r.Pages() > 1 && (angle == Angle90 || angle == Angle270) {
if err := r.SetPageHeight(width); err != nil {
return err
}
}
return nil
}

Expand All @@ -1494,6 +1589,16 @@ func (r *ImageRef) Similarity(scale float64, angle float64, backgroundColor *Col
return nil
}

// Grid tiles the image pages into a matrix across*down
func (r *ImageRef) Grid(tileHeight, across, down int) error {
out, err := vipsGrid(r.image, tileHeight, across, down)
if err != nil {
return err
}
r.setImage(out)
return nil
}

// SmartCrop will crop the image based on interesting factor
func (r *ImageRef) SmartCrop(width int, height int, interesting Interesting) error {
out, err := vipsSmartCrop(r.image, width, height, interesting)
Expand Down Expand Up @@ -1595,6 +1700,25 @@ const (
CodingRAD Coding = C.VIPS_CODING_RAD
)

func (r *ImageRef) newMetadata(format ImageType) *ImageMetadata {
return &ImageMetadata{
Format: format,
Width: r.Width(),
Height: r.Height(),
Colorspace: r.ColorSpace(),
Orientation: r.Orientation(),
Pages: r.Pages(),
}
}

func (r *ImageRef) multiPageNotSupported() error {
if r.Pages() > 1 {
return ErrUnsupportedMultiPageOperation
}

return nil
}

// Pixelate applies a simple pixelate filter to the image
func Pixelate(imageRef *ImageRef, factor float64) (err error) {
if factor < 1 {
Expand Down

0 comments on commit 623c60d

Please sign in to comment.