Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions magick.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,22 @@ func (im *MagickImage) Height() int {
return (int)(im.Image.rows)
}

// VirtualCanvasWidth returns the virtual canvas width.
//
// This can be different than the image's width for certain formats, such as
// GIF. See PlusRepage() for more information.
func (im *MagickImage) VirtualCanvasWidth() int {
return (int)(im.Image.page.width)
}

// VirtualCanvasHeight returns the virtual canvas height.
//
// This can be different than the image's height for certain formats, such as
// GIF. See PlusRepage() for more information.
func (im *MagickImage) VirtualCanvasHeight() int {
return (int)(im.Image.page.height)
}

// Type returns the underlying encoding or "magick" of the image as a string
func (im *MagickImage) Type() (t string) {
return strings.Trim(string(C.GoBytes(unsafe.Pointer(&im.Image.magick), 4096)), "\x00")
Expand Down Expand Up @@ -318,6 +334,31 @@ func (im *MagickImage) SetProperty(prop, value string) (err error) {
return
}

// PlusRepage runs the same logic as the +repage command line argument.
//
// This removes the virtual canvas metadata. It resets the canvas to match what
// you cropped.
//
// After cropping you may find that the section you cropped is present at the
// location you cropped it at surrounded on all sides by empty space/pixels.
// This apparently occurs when the image format allows positioning sections of
// the image via offsets, such as GIF.
//
// See http://www.imagemagick.org/script/command-line-options.php#repage and
// http://www.imagemagick.org/Usage/crop/#crop_page
//
// The latter says: "Always use "+repage" after any 'crop' like operation.
// Unless you actually need to preserve that info." This means it might be
// useful for Crop() to call this function.
func (im *MagickImage) PlusRepage() {
geom := C.CString("0x0+0+0")
defer C.free(unsafe.Pointer(geom))

// +repage is a command line option that has no direct API equivalent. See
// MagickWand/operation.c for the implementation. I copy that here.
C.ParseAbsoluteGeometry(geom, &im.Image.page)
}

// ParseGeometryToRectangleInfo converts from a geometry string (WxH+X+Y) into a Magick
// RectangleInfo that contains the individual properties
func (im *MagickImage) ParseGeometryToRectangleInfo(geometry string) (info C.RectangleInfo, err error) {
Expand Down Expand Up @@ -458,6 +499,24 @@ func (im *MagickImage) Strip() (err error) {
return
}

// AutoOrient adjusts an image so that its orientation is suitable for
// viewing (i.e. top-left orientation).
func (im *MagickImage) AutoOrient() error {
exception := C.AcquireExceptionInfo()
defer C.DestroyExceptionInfo(exception)

newImage := C.AutoOrientImage(im.Image, im.Image.orientation, exception)

failed := C.CheckException(exception)
if failed == C.MagickTrue {
return ErrorFromExceptionInfo(exception)
}

im.ReplaceImage(newImage)

return nil
}

// ToBlob takes a (transformed) MagickImage and returns a byte slice in the format you specify with extension.
// Magick uses the extension to transform the image in to the proper encoding (e.g. "jpg", "png")
func (im *MagickImage) ToBlob(extension string) (blob []byte, err error) {
Expand Down
133 changes: 132 additions & 1 deletion magick_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package magick

import (
"github.com/bmizerany/assert"
"fmt"
"io/ioutil"
"log"
"os"
"testing"

"github.com/bmizerany/assert"
)

func setupImage(t *testing.T) (image *MagickImage) {
Expand Down Expand Up @@ -170,6 +172,82 @@ func TestCrop(t *testing.T) {
assert.T(t, err != nil)
}

// The use case for +repage is that we need to reset an image's canvas so only
// the cropped portion is present. Cropping and not using +repage leaves the
// canvas the same size, but with only the cropped portion's pixels remaining.
// I believe this is only the case for certain image formats such as GIF and
// PNG.
//
// Test to demonstrate the problem and show how PlusRepage() solves it.
func TestPlusRepage(t *testing.T) {
filename := "test/heart_original.png"
image, err := NewFromFile(filename)
if err != nil {
t.Errorf("NewFromFile(%s) error = %s", filename, err)
return
}

origWidth := image.Width()
origHeight := image.Height()
origVirtualCanvasWidth := image.VirtualCanvasWidth()
origVirtualCanvasHeight := image.VirtualCanvasHeight()

wantedWidth := 100
wantedHeight := 100
geom := fmt.Sprintf("%dx%d!+10+10", wantedWidth, wantedHeight)
if err := image.Crop(geom); err != nil {
t.Errorf("image.Crop(%s) error = %s", geom, err)
_ = image.Destroy()
return
}

if image.Width() != wantedWidth || image.Height() != wantedHeight {
t.Errorf("image width x height after cropping is %dx%d, wanted %dx%d",
image.Width(), image.Height(), origWidth, origHeight)
_ = image.Destroy()
return
}

// The virtual canvas width x height should currently be as it was. The
// cropped region will be surrounded by empty space if we save it now.
if image.VirtualCanvasWidth() != origVirtualCanvasWidth ||
image.VirtualCanvasHeight() != origVirtualCanvasHeight {
t.Errorf("virtual canvas width x height after cropping is %dx%d, wanted %dx%d",
image.VirtualCanvasWidth(), image.VirtualCanvasHeight(),
origVirtualCanvasWidth, origVirtualCanvasHeight)
_ = image.Destroy()
return
}

// Reset the virtual canvas using +repage. This removes virtual canvas
// information. The effect of removing it means the image, once saved, will
// have the dimensions of the image region we cropped.
image.PlusRepage()

// Check both sets of dimensions again. The image size should still be the
// same. The virtual canvas size should be 0x0 since we removed information
// about it.

if image.Width() != wantedWidth || image.Height() != wantedHeight {
t.Errorf("image width x height after cropping is %dx%d, wanted %dx%d",
image.Width(), image.Height(), origWidth, origHeight)
_ = image.Destroy()
return
}

if image.VirtualCanvasWidth() != 0 || image.VirtualCanvasHeight() != 0 {
t.Errorf("virtual canvas width x height after +repage is %dx%d, wanted %dx%d",
image.VirtualCanvasWidth(), image.VirtualCanvasHeight(), 0, 0)
_ = image.Destroy()
return
}

if err := image.Destroy(); err != nil {
t.Errorf("image.Destroy() error = %s", err)
return
}
}

func TestShadow(t *testing.T) {
image := setupImage(t)
err := image.Shadow("#000", 75, 2, 0, 0)
Expand Down Expand Up @@ -330,3 +408,56 @@ func TestFullStack(t *testing.T) {
assert.T(t, err == nil)
}
}

func TestAutoOrientNeedsToRotate(t *testing.T) {
// This file is set so the heart appears to need to be rotated 90 degrees
// clockwise. It has EXIF orientation set to indicate that.
//
// I created it from test/heart_original.png this way:
// convert -rotate 270 heart_original.png heart_rotated.jpg
// exiftool -Orientation=6 -n heart_rotated.jpg
filename := "test/heart_rotated.jpg"

image, err := NewFromFile(filename)
if err != nil {
t.Errorf("NewFromFile(%s) = %s", filename, err)
return
}

err = image.AutoOrient()
if err != nil {
_ = image.Destroy()
t.Errorf("AutoOrient() = %s", err)
return
}

err = image.Destroy()
if err != nil {
t.Errorf("Destroy() = %s", err)
return
}
}

func TestAutoOrientNoNeedToRotate(t *testing.T) {
// This file does not need to change.
filename := "test/heart_original.png"

image, err := NewFromFile(filename)
if err != nil {
t.Errorf("NewFromFile(%s) = %s", filename, err)
return
}

err = image.AutoOrient()
if err != nil {
_ = image.Destroy()
t.Errorf("AutoOrient() = %s", err)
return
}

err = image.Destroy()
if err != nil {
t.Errorf("Destroy() = %s", err)
return
}
}
Binary file added test/heart_rotated.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.