Documentation
¶
Overview ¶
Package imagemagick provides a high-level wrapper for the ImageMagick `convert` command and a replacement for the `identify` command to gather detailed information on images like width, height, exif tags, colorspace, etc, without requiring the ImageMagick shared libraries; works on Linux, Windows, MacOS and any other system that has access to the `convert` command.
Example (GetImageDetails) ¶
An example of getting image details from an image file. For this example we're downloading the file from w3.org, writing it to a temp file and getting its details
package main import ( "fmt" "io/ioutil" "net/http" "os" "github.com/kamermans/imagemagick" ) // An example of getting image details from an image file. For this example we're downloading the // file from w3.org, writing it to a temp file and getting its details func main() { var ( convertCmd = `/usr/local/bin/convert` imageURL = `https://www.w3.org/People/mimasa/test/imgformat/img/w3c_home.gif` ) imageFile, err := downloadTempImage(imageURL) if err != nil { panic("Could not download the example image") } defer os.Remove(imageFile) parser := imagemagick.NewParser() parser.ConvertCommand = convertCmd results, detErr := parser.GetImageDetails(imageFile) if detErr != nil { panic(detErr.Error()) } // Note that one output JSON can contain multiple results image := results[0].Image // Print the format fmt.Printf("Format: %v (%v)\n", image.Format, image.MimeType) // Print the geometry fmt.Printf("Dimensions: %v\n", *image.Geometry.Dimensions) // Example output: // Format: GIF (image/gif) // Dimensions: {Width: 72, Height: 48} } func downloadTempImage(imageURL string) (file string, err error) { resp, err := http.Get(imageURL) if err != nil { return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return } fh, err := ioutil.TempFile("", "imagemagick_example_") if err != nil { return } defer fh.Close() _, err = fh.Write(body) if err != nil { return } file = fh.Name() return }
Example (GetImageDetailsFromJSON) ¶
Example of getting image details from the ImageMagick `convert` tool's `json` format
package main import ( "fmt" "sort" "github.com/kamermans/imagemagick" ) // Example of getting image details from the ImageMagick `convert` tool's `json` format func main() { jsonBlob := getSampleJSONOutput() parser := imagemagick.NewParser() results, err := parser.GetImageDetailsFromJSON(jsonBlob) if err != nil { panic(err.Error()) } // Note that one output JSON can contain multiple results image := results[0].Image // Print the filename fmt.Printf("Image: %v (%v)\n", image.BaseName, image.Format) // Collect the ICC information props := image.PropertiesMap() icc := props["icc"] // Sort ICC tags and print them in order so the tests don't fail :) propTags := []string{} for tag := range icc { propTags = append(propTags, tag) } sort.Strings(propTags) // Print the ICC information for _, tag := range propTags { fmt.Printf("ICC %v: %v\n", tag, icc[tag]) } // Print the geometry fmt.Printf("Geometry: %v\n", *image.Geometry) // Print the profile size profileSizePct := image.ProfileSizePercent() * 100.0 fmt.Printf("Profiles: The embedded profiles account for %.2f%% of the total file size\n", profileSizePct) } func getSampleJSONOutput() *[]byte { data := []byte(sampleJSONOutput) return &data } // This is an example of the output you get from ImageMagick `convert` // when you use `json` for the output format. For example, to get the // JSON details for `foo.jpg`, you would use this command: // // convert foo.jpg foo_details.json // // This package uses the STDOUT method to avoid writing an output file: // // convert foo.jpg json:- const sampleJSONOutput = `[{ "image": { "name": "json:/tmp/image_metadata_multi_formats_linux.json", "baseName": "bug72278.jpg", "format": "JPEG", "formatDescription": "JPEG", "mimeType": "image/jpeg", "class": "DirectClass", "geometry": { "width": 300, "height": 300, "x": 0, "y": 0 }, "resolution": { "x": 200, "y": 200 }, "printSize": { "x": 1.5, "y": 1.5 }, "units": "PixelsPerInch", "type": "Bilevel", "baseType": "Undefined", "endianess": "Undefined", "colorspace": "sRGB", "depth": 1, "baseDepth": 8, "channelDepth": { "red": 1, "green": 1, "blue": 1 }, "pixels": 270000, "imageStatistics": { "Overall": { "min": 255, "max": 255, "mean": 255, "standardDeviation": 0, "kurtosis": 1.6384e+64, "skewness": 9.375e+44, "entropy": -nan } }, "channelStatistics": { "Red": { "min": 255, "max": 255, "mean": 255, "standardDeviation": 0, "kurtosis": 8.192e+63, "skewness": 1e+45, "entropy": -nan }, "Green": { "min": 255, "max": 255, "mean": 255, "standardDeviation": 0, "kurtosis": 8.192e+63, "skewness": 1e+45, "entropy": -nan }, "Blue": { "min": 255, "max": 255, "mean": 255, "standardDeviation": 0, "kurtosis": 8.192e+63, "skewness": 1e+45, "entropy": -nan } }, "renderingIntent": "Perceptual", "gamma": 0.454545, "chromaticity": { "redPrimary": { "x": 0.64, "y": 0.33 }, "greenPrimary": { "x": 0.3, "y": 0.6 }, "bluePrimary": { "x": 0.15, "y": 0.06 }, "whitePrimary": { "x": 0.3127, "y": 0.329 } }, "matteColor": "#BDBDBD", "backgroundColor": "#FFFFFF", "borderColor": "#DFDFDF", "transparentColor": "#00000000", "interlace": "None", "intensity": "Undefined", "compose": "Over", "pageGeometry": { "width": 300, "height": 300, "x": 0, "y": 0 }, "dispose": "Undefined", "iterations": 0, "scene": 13, "scenes": 26, "compression": "None", "quality": 79, "orientation": "Undefined", "properties": { "comment": "Test", "date:create": "2017-10-19T10:30:02-04:00", "date:modify": "2017-10-19T10:30:02-04:00", "exif:ColorSpace": "1", "exif:ComponentsConfiguration": "1, 2, 3, 0", "exif:Copyright": "Test", "exif:DateTime": "2008:04:03 11:06:23", "exif:ExifImageLength": "300", "exif:ExifImageWidth": "300", "exif:ExifOffset": "196", "exif:ExifVersion": "48, 50, 50, 48", "exif:FlashPixVersion": "48, 49, 48, 48", "exif:ResolutionUnit": "2", "exif:Software": "Paint Shop Pro Photo 12.00", "exif:thumbnail:Compression": "6", "exif:thumbnail:JPEGInterchangeFormat": "380", "exif:thumbnail:JPEGInterchangeFormatLength": "1325", "exif:thumbnail:ResolutionUnit": "2", "exif:thumbnail:XResolution": "787399/10000", "exif:thumbnail:YCbCrPositioning": "2", "exif:thumbnail:YResolution": "787399/10000", "exif:XResolution": "1999995/10000", "exif:YCbCrPositioning": "2", "exif:YResolution": "1999995/10000", "icc:copyright": "Copyright (c) 1998 Hewlett-Packard Company", "icc:description": "sRGB IEC61966-2.1", "icc:manufacturer": "IEC http://www.iec.ch", "icc:model": "IEC 61966-2.1 Default RGB colour space - sRGB", "jpeg:colorspace": "2", "jpeg:sampling-factor": "1x1,1x1,1x1", "signature": "31fed455c2bb6e7258a946a2adc33d8493c2084346b9bb000e5042977c56221e" }, "profiles": { "8bim": { "length": 28 }, "exif": { "length": 1717 }, "icc": { "length": 7261 }, "iptc": { "Unknown[2,0]": [null], "Copyright String[2,116]": ["Test"], "length": 16 } }, "tainted": false, "filesize": "45720B", "numberPixels": "90000", "pixelsPerSecond": "692308B", "userTime": "0.150u", "elapsedTime": "0:01.129", "version": "/usr/local/share/doc/ImageMagick-7//index.html" } }]`
Output: Image: bug72278.jpg (JPEG) ICC copyright: Copyright (c) 1998 Hewlett-Packard Company ICC description: sRGB IEC61966-2.1 ICC manufacturer: IEC http://www.iec.ch ICC model: IEC 61966-2.1 Default RGB colour space - sRGB Geometry: {{X: 0, Y: 0} {Width: 300, Height: 300}} Profiles: The embedded profiles account for 16.48% of the total file size
Example (GetImageDetailsParallel) ¶
Example of getting the image details for all the files in a given directory in parallel, utilizing all the available CPUs in the machine. It also includes a progress function that shows the status of the job every 2 seconds. This example runs on Linux, Windows and MacOS
package main import ( "fmt" "os" "path/filepath" "sort" "strings" "time" "github.com/kamermans/imagemagick" ) func main() { //func Test_getImageDetailsParallel(t *testing.T) { var ( convertCmd = `c:\ImageMagick\convert.exe` imageFilesDir = `c:\data\sample_images` ) parser := imagemagick.NewParser() parser.ConvertCommand = convertCmd files := make(chan string) results := make(chan *imagemagick.ImageResult) errs := make(chan *imagemagick.ParserError) // Used to tell us when the results have all be consumed done := make(chan bool) parser.GetImageDetailsParallel(files, results, errs) // Send in files go func() { defer close(files) filepath.Walk(imageFilesDir, func(path string, info os.FileInfo, err error) error { if err != nil { fmt.Printf("Unable to access path %q: %v\n", imageFilesDir, err) return err } if info.IsDir() { return nil } // Send this image into the files channel files <- path return nil }) }() numErrors := 0 numResults := 0 startTime := time.Now() // Store the number of images of each format that we've seen resultsByFormat := map[string]int64{} // Store the total size of the images we've seen totalSize := int64(0) // Report progress this often reportInterval := 2 * time.Second // Report progress go func() { time.Sleep(reportInterval) for { // Get a sorted list of formats so it looks consistent formats := []string{} for format := range resultsByFormat { formats = append(formats, format) } sort.Strings(formats) outLines := []string{} for _, format := range formats { outLines = append(outLines, fmt.Sprintf("%v: %v", format, resultsByFormat[format])) } numPerSecond := float64(numResults+numErrors) / time.Since(startTime).Seconds() fmt.Printf("Results: %v, Errors: %v, Rate: %.0f/sec, Image Data: %v MB, Formats: {%v}\n", numResults, numErrors, numPerSecond, totalSize/1000000, strings.Join(outLines, ", "), ) time.Sleep(reportInterval) } }() // Consume results and errors go func() { moreErrs := true moreResults := true for { if !moreErrs && !moreResults { break } select { case _, ok := <-errs: if !ok { moreErrs = false continue } numErrors++ case details, ok := <-results: if !ok { moreResults = false continue } numResults++ image := details.Image // Collect some stats for the progress function above totalSize += image.Size() if details.Image.Format != "" { resultsByFormat[details.Image.Format]++ } // You can get the image details here if you want // fmt.Printf("Received result for image: %v (%v)\n", // image.BaseName, // image.Format, // ) } } done <- true }() // Wait for all the results and errors to be consumed <-done fmt.Printf("Received %v results and %v errors\n", numResults, numErrors) // Here's what the output looks like on my laptop with 4523 sample images: // // Results: 40, Errors: 0, Rate: 20/sec, Image Data: 3 MB, Formats: {JPEG: 40} // Results: 160, Errors: 0, Rate: 40/sec, Image Data: 9 MB, Formats: {JPEG: 159, PNG: 1} // Results: 280, Errors: 0, Rate: 46/sec, Image Data: 18 MB, Formats: {JPEG: 279, PNG: 1} // ... lots of output ... // Results: 4304, Errors: 16, Rate: 46/sec, Image Data: 303 MB, Formats: {JPEG: 4281, PNG: 23} // Results: 4386, Errors: 17, Rate: 46/sec, Image Data: 305 MB, Formats: {JPEG: 4362, PNG: 24} // Results: 4465, Errors: 19, Rate: 46/sec, Image Data: 309 MB, Formats: {JPEG: 4440, PNG: 25} // Received 4503 results and 20 errors }
Index ¶
- type ChannelStatistics
- type Dimensions
- type Geometry
- type ImageDetails
- func (details ImageDetails) ExifTags() map[string]string
- func (details ImageDetails) HasProfile(name string) bool
- func (details ImageDetails) ProfileNames() (names []string)
- func (details ImageDetails) ProfileSizePercent() float64
- func (details ImageDetails) ProfileSizes() (lengths map[string]int64)
- func (details ImageDetails) ProfileTotalSize() (size int64)
- func (details ImageDetails) PropertiesMap(tagFilter ...string) map[string]map[string]string
- func (details ImageDetails) Size() int64
- func (details *ImageDetails) ToJSON(pretty bool) (out []byte, err error)
- type ImageResult
- type Parser
- func (parser *Parser) Convert(args ...string) (stdOut *[]byte, stdErr *[]byte, err *ParserError)
- func (parser *Parser) GetImageDetails(files ...string) (results []*ImageResult, err *ParserError)
- func (parser *Parser) GetImageDetailsFromJSON(jsonBlob *[]byte) (results []*ImageResult, err error)
- func (parser *Parser) GetImageDetailsParallel(files <-chan string, results chan<- *ImageResult, errs chan<- *ParserError)
- func (parser *Parser) SetCommand(command func(name string, arg ...string) *exec.Cmd)
- type ParserError
- type Point
- type PointFloat
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ChannelStatistics ¶
type ChannelStatistics struct { Min float64 `json:"min"` // 0, Max float64 `json:"max"` // 255, Mean float64 `json:"mean"` // 187.475, StandardDeviation float64 `json:"standardDeviation"` // 90.9415, Kurtosis float64 `json:"kurtosis"` // -1.22588, Skewness float64 `json:"skewness"` // -0.755169, Entropy float64 `json:"entropy"` // 0.515529 }
ChannelStatistics represents the image color channel statistics
type Dimensions ¶
Dimensions represents box dimensions with Width and Height
type Geometry ¶
type Geometry struct { *Point *Dimensions }
Geometry represents image geometry, including a Point{X, Y} offset and Dimensions{Width, Height} dimensions
func (Geometry) Canvas ¶
func (geo Geometry) Canvas() *Dimensions
Canvas is the total width and height of the canvas (width/height + x/y offset)
type ImageDetails ¶
type ImageDetails struct { Alpha string `json:"alpha"` //"#00FF0000" BackgroundColor string `json:"backgroundColor"` //"#FFFFFF" BaseDepth int64 `json:"baseDepth"` //8 BaseName string `json:"baseName"` //"image_0002c93a9c0c53e7379a4524fa953ebb" BaseType string `json:"baseType"` //"Undefined" BorderColor string `json:"borderColor"` //"#DFDFDF" ChannelDepth map[string]int64 `json:"channelDepth"` // ChannelStatistics map[string]*ChannelStatistics `json:"channelStatistics"` // Chromaticity map[string]*PointFloat `json:"chromaticity"` // Class string `json:"class"` //"DirectClass" Colormap []string `json:"colormap"` //["#7F82B8FF","#393747FF"] ColormapEntries int64 `json:"colormapEntries"` //128 Colorspace string `json:"colorspace"` //"sRGB" Compose string `json:"compose"` //"Over" Compression string `json:"compression"` //"JPEG2000" Depth int64 `json:"depth"` //8 Dispose string `json:"dispose"` //"Undefined" ElapsedTime string `json:"elapsedTime"` //"0:01.049" Endianess string `json:"endianess"` //"Undefined" Filesize string `json:"filesize"` //"0B" Format string `json:"format"` //"JP2" FormatDescription string `json:"formatDescription"` //"JP2" Gamma float64 `json:"gamma"` //0.454545 Geometry *Geometry `json:"geometry"` // ImageStatistics map[string]*ChannelStatistics `json:"imageStatistics"` // Intensity string `json:"intensity"` //"Undefined" Interlace string `json:"interlace"` //"None" Iterations int64 `json:"iterations"` //0 MatteColor string `json:"matteColor"` //"#BDBDBD" MimeType string `json:"mimeType"` //"image/jp2" Name string `json:"name"` //"test.json" NumberPixels int64 `json:"numberPixels,string"` //"211750" Orientation string `json:"orientation"` //"Undefined" PageGeometry *Geometry `json:"pageGeometry"` // Pixels int64 `json:"pixels"` //635250 PixelsPerSecond string `json:"pixelsPerSecond"` //"4235000B" PrintSize *PointFloat `json:"printSize"` //{"x": 2.08333,"y": 1.04167} Profiles map[string]map[string]interface{} `json:"profiles"` // Properties map[string]string `json:"properties"` // Quality int64 `json:"quality"` //75 RenderingIntent string `json:"renderingIntent"` //"Perceptual" Resolution *PointFloat `json:"resolution"` //{"x": 96,"y": 96} Scene int64 `json:"scene"` //12 Scenes int64 `json:"scenes"` //26 Tainted bool `json:"tainted"` //false TransparentColor string `json:"transparentColor"` //"#00000000" Type string `json:"type"` //"TrueColor" Units string `json:"units"` //"Undefined" UserTime string `json:"userTime"` //"0.030u" Version string `json:"version"` //"/usr/local/share/doc/ImageMagick-7//index.html" }
ImageDetails provides detailed information on the image, there are many helpful methods on this object
func (ImageDetails) ExifTags ¶
func (details ImageDetails) ExifTags() map[string]string
ExifTags returns a map of EXIF tags to their values. These are pulled from the Properties slice. Note that the prefix "exif:" is timmed from the tag name. An empty map is returned if there are no EXIF tags present
func (ImageDetails) HasProfile ¶
func (details ImageDetails) HasProfile(name string) bool
HasProfile returns true if the image has an embedded profile of the given type. Possible options include, but are not limited to: 8bim, exif, iptc, xmp, icc, app1, app12 Note that zero-length profiles will return false
func (ImageDetails) ProfileNames ¶
func (details ImageDetails) ProfileNames() (names []string)
ProfileNames of the embedded profiles. Note that all profiles are included, even if they are zero-length
func (ImageDetails) ProfileSizePercent ¶
func (details ImageDetails) ProfileSizePercent() float64
ProfileSizePercent returns the percentage of the total filesize which is used by the profiles
func (ImageDetails) ProfileSizes ¶
func (details ImageDetails) ProfileSizes() (lengths map[string]int64)
ProfileSizes returns a map of embedded profile names to their size in bytes
func (ImageDetails) ProfileTotalSize ¶
func (details ImageDetails) ProfileTotalSize() (size int64)
ProfileTotalSize returns the total byte size of all the embedded profiles
func (ImageDetails) PropertiesMap ¶
func (details ImageDetails) PropertiesMap(tagFilter ...string) map[string]map[string]string
PropertiesMap returns a map of the image Properties. The key is split on the first ":" and grouped by the first half (the tag name) so the map is a map of map[string]string like this:
{ "icc": { "brand": "Canon", "model": "EOS 5D Mark IV", }, "exif": { "Software": "Adobe Photoshop CC 2017 (Macintosh)", }, }
func (ImageDetails) Size ¶
func (details ImageDetails) Size() int64
Size of the image in bytes. ImageMagick returns a strangely-formatted string and this the in64 equivalent
type ImageResult ¶
type ImageResult struct {
Image *ImageDetails `json:"image"`
}
ImageResult is the top-Level result from ImageMagick. You almost certainly want to access the Image property, but this wrapper is left here for future use, should other types by introduced
type Parser ¶
type Parser struct { // The 'convert' command ConvertCommand string // Number of files to pass to convert at once when running in parallel BatchSize int // Number of workers to start when running in parallel (default: # of CPUs) Workers int // contains filtered or unexported fields }
Parser represents an ImageMagick command-line tool parser
func (*Parser) Convert ¶
func (parser *Parser) Convert(args ...string) (stdOut *[]byte, stdErr *[]byte, err *ParserError)
Convert is a helper to call the ImageMagick `convert` command. It will return the stdOut, stdErr and a ParserError if the command failed (by returing a non-zero exit code, for example)
func (*Parser) GetImageDetails ¶
func (parser *Parser) GetImageDetails(files ...string) (results []*ImageResult, err *ParserError)
GetImageDetails computes ImageDetails for one or more input files, returning (results, err). If an error is encountered, results will be nil and err will contain the error.
func (*Parser) GetImageDetailsFromJSON ¶
func (parser *Parser) GetImageDetailsFromJSON(jsonBlob *[]byte) (results []*ImageResult, err error)
GetImageDetailsFromJSON computes ImageDetails for the given JSON data, returning (results, err). If an error is encountered, results will be nil and err will contain the error. Note that the JSON data is cleaned of invalid numbers with Regexp because ImageMagick `convert` leaks C++ NaNs into the output data, like `{"bytes": -nan}` and `{"entropy": -1.#IND}`
func (*Parser) GetImageDetailsParallel ¶
func (parser *Parser) GetImageDetailsParallel( files <-chan string, results chan<- *ImageResult, errs chan<- *ParserError, )
GetImageDetailsParallel computes ImageDetails for a channel of input files. The results are available in the results channel and errors are on the errors channel. You should read the results and errors channels in a go routine to prevent blocking. The number of workers is defined at Parser.Workers. ImageMagick supports batches of input files, and this function uses batches of size Parse.BatchSize. When a batch of files is passed to ImageMagick and an error is encountered, the batch is split up and each file is sent individually so the bad file can be identified and sent to the errors channel.
type ParserError ¶
type ParserError struct {
// contains filtered or unexported fields
}
ParserError represents an error by the parser
func NewParserError ¶
NewParserError creates a new ParserError
func (*ParserError) Cmd ¶
func (err *ParserError) Cmd() string
Cmd returns the command that caused the error (if any)
func (*ParserError) Error ¶
func (err *ParserError) Error() string
Error returns a string representation of the error with all of its properties
func (*ParserError) File ¶
func (err *ParserError) File() string
File returns the file that caused the error (if any)
func (*ParserError) StdErr ¶
func (err *ParserError) StdErr() []byte
StdErr that was produced by the failed command (if any)
func (*ParserError) StdOut ¶
func (err *ParserError) StdOut() []byte
StdOut that was produced by the failed command (if any)
type PointFloat ¶
PointFloat represents a float64 X, Y point / coordinate