Browse Source

ICO support

DarthSim 6 years ago
parent
commit
4549089183

+ 11 - 1
Gopkg.lock

@@ -173,6 +173,14 @@
   pruneopts = "UT"
   revision = "ae77be60afb1dcacde03767a8c37337fad28ac14"
 
+[[projects]]
+  digest = "1:d33e9f151ad66f61e320511ee3a2189178b78e2d1e93f098801b4ecf6154474a"
+  name = "github.com/mat/besticon"
+  packages = ["ico"]
+  pruneopts = "UT"
+  revision = "257c937cf48f690c78c900f895e9fc4027962ac2"
+  version = "v3.8.0"
+
 [[projects]]
   branch = "master"
   digest = "1:97e35d2efabea52337e58f96e8b798fd461bf42d2519beb50f485029c6ad6aa5"
@@ -314,9 +322,10 @@
   version = "v0.18.0"
 
 [[projects]]
-  digest = "1:72b36febaabad58e1864de2b43de05689ce27a2c9a582a61a25e71a31ba23d0b"
+  digest = "1:16d8d746fb42f8318537e77dbf22745c180db4f5b61b59a89e0fb8101bb3303e"
   name = "golang.org/x/image"
   packages = [
+    "bmp",
     "riff",
     "vp8",
     "vp8l",
@@ -494,6 +503,7 @@
     "github.com/aws/aws-sdk-go/service/s3",
     "github.com/bugsnag/bugsnag-go",
     "github.com/honeybadger-io/honeybadger-go",
+    "github.com/mat/besticon/ico",
     "github.com/matoous/go-nanoid",
     "github.com/newrelic/go-agent",
     "github.com/prometheus/client_golang/prometheus",

+ 6 - 1
docs/image_formats_support.md

@@ -5,7 +5,8 @@ At the moment, imgproxy supports only the most popular Web image formats:
 * PNG;
 * JPEG;
 * WebP;
-* GIF.
+* GIF;
+* ICO.
 
 ## GIF support
 
@@ -16,3 +17,7 @@ Since processing of animated GIFs is pretty heavy, only one frame is processed b
 * `IMGPROXY_MAX_GIF_FRAMES`: the maximum of animated GIF frames to being processed. Default: `1`.
 
 **Note:** imgproxy summarizes all GIF frames resolutions while checking source image resolution.
+
+## ICO support
+
+imgproxy supports ICO output only when using libvips 8.7.0+ compiled with ImageMagick support. Official imgproxy Docker image supports ICO out of the box.

+ 1 - 0
download.go

@@ -16,6 +16,7 @@ import (
 	_ "image/jpeg"
 	_ "image/png"
 
+	_ "github.com/mat/besticon/ico"
 	_ "golang.org/x/image/webp"
 )
 

+ 28 - 0
ico_data.go

@@ -0,0 +1,28 @@
+package main
+
+import (
+	"bytes"
+	"image"
+	"image/draw"
+
+	_ "github.com/mat/besticon/ico"
+)
+
+func icoData(data []byte) (out []byte, width int, height int, err error) {
+	var ico image.Image
+
+	ico, _, err = image.Decode(bytes.NewReader(data))
+	if err != nil {
+		return
+	}
+
+	// Ensure that image is in RGBA format
+	rgba := image.NewRGBA(ico.Bounds())
+	draw.Draw(rgba, ico.Bounds(), ico, image.ZP, draw.Src)
+
+	width = rgba.Bounds().Dx()
+	height = rgba.Bounds().Dy()
+	out = rgba.Pix
+
+	return
+}

+ 2 - 1
image_types.h

@@ -3,5 +3,6 @@ enum types {
   JPEG,
   PNG,
   WEBP,
-  GIF
+  GIF,
+  ICO
 };

+ 15 - 0
process.go

@@ -74,6 +74,9 @@ func initVips() {
 		vipsTypeSupportLoad[imageTypeGIF] = true
 	}
 
+	// we load ICO with github.com/mat/besticon/ico and send decoded data to vips
+	vipsTypeSupportLoad[imageTypeICO] = true
+
 	if int(C.vips_type_find_save_go(C.int(imageTypeJPEG))) != 0 {
 		vipsTypeSupportSave[imageTypeJPEG] = true
 	}
@@ -86,6 +89,9 @@ func initVips() {
 	if int(C.vips_type_find_save_go(C.int(imageTypeGIF))) != 0 {
 		vipsTypeSupportSave[imageTypeGIF] = true
 	}
+	if int(C.vips_type_find_save_go(C.int(imageTypeICO))) != 0 {
+		vipsTypeSupportSave[imageTypeICO] = true
+	}
 
 	if conf.JpegProgressive {
 		cConf.JpegProgressive = C.int(1)
@@ -606,6 +612,13 @@ func vipsLoadImage(data []byte, imgtype imageType, shrink int, allPages bool) (*
 		err = C.vips_webpload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(shrink), &img)
 	case imageTypeGIF:
 		err = C.vips_gifload_go(unsafe.Pointer(&data[0]), C.size_t(len(data)), pages, &img)
+	case imageTypeICO:
+		rawData, width, height, icoErr := icoData(data)
+		if icoErr != nil {
+			return nil, icoErr
+		}
+
+		img = C.vips_image_new_from_memory_copy(unsafe.Pointer(&rawData[0]), C.size_t(width*height*4), C.int(width), C.int(height), 4, C.VIPS_FORMAT_UCHAR)
 	}
 	if err != 0 {
 		return nil, vipsError()
@@ -634,6 +647,8 @@ func vipsSaveImage(img *C.struct__VipsImage, imgtype imageType, quality int) ([]
 		err = C.vips_webpsave_go(img, &ptr, &imgsize, 1, C.int(quality))
 	case imageTypeGIF:
 		err = C.vips_gifsave_go(img, &ptr, &imgsize)
+	case imageTypeICO:
+		err = C.vips_icosave_go(img, &ptr, &imgsize)
 	}
 	if err != 0 {
 		return nil, vipsError()

+ 2 - 0
processing_options.go

@@ -28,6 +28,7 @@ const (
 	imageTypePNG     = imageType(C.PNG)
 	imageTypeWEBP    = imageType(C.WEBP)
 	imageTypeGIF     = imageType(C.GIF)
+	imageTypeICO     = imageType(C.ICO)
 )
 
 type processingHeaders struct {
@@ -43,6 +44,7 @@ var imageTypes = map[string]imageType{
 	"png":  imageTypePNG,
 	"webp": imageTypeWEBP,
 	"gif":  imageTypeGIF,
+	"ico":  imageTypeICO,
 }
 
 type gravityType int

+ 2 - 0
server.go

@@ -25,6 +25,7 @@ var (
 		imageTypePNG:  "image/png",
 		imageTypeWEBP: "image/webp",
 		imageTypeGIF:  "image/gif",
+		imageTypeICO:  "image/x-icon",
 	}
 
 	contentDispositions = map[imageType]string{
@@ -32,6 +33,7 @@ var (
 		imageTypePNG:  "inline; filename=\"image.png\"",
 		imageTypeWEBP: "inline; filename=\"image.webp\"",
 		imageTypeGIF:  "inline; filename=\"image.gif\"",
+		imageTypeICO:  "inline; filename=\"favicon.ico\"",
 	}
 
 	authHeaderMust []byte

+ 21 - 0
vendor/github.com/mat/besticon/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015-2018 Matthias Lüdtke, Hamburg - https://github.com/mat
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

BIN
vendor/github.com/mat/besticon/ico/addthis.ico


BIN
vendor/github.com/mat/besticon/ico/besticon.ico


BIN
vendor/github.com/mat/besticon/ico/broken.ico


BIN
vendor/github.com/mat/besticon/ico/codeplex.ico


BIN
vendor/github.com/mat/besticon/ico/favicon.ico


BIN
vendor/github.com/mat/besticon/ico/github.ico


+ 261 - 0
vendor/github.com/mat/besticon/ico/ico.go

@@ -0,0 +1,261 @@
+// Package ico registers image.Decode and DecodeConfig support
+// for the icon (container) format.
+package ico
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"image"
+	"io"
+	"io/ioutil"
+
+	"image/png"
+
+	"golang.org/x/image/bmp"
+)
+
+type icondir struct {
+	Reserved uint16
+	Type     uint16
+	Count    uint16
+	Entries  []icondirEntry
+}
+
+type icondirEntry struct {
+	Width        byte
+	Height       byte
+	PaletteCount byte
+	Reserved     byte
+	ColorPlanes  uint16
+	BitsPerPixel uint16
+	Size         uint32
+	Offset       uint32
+}
+
+func (dir *icondir) FindBestIcon() *icondirEntry {
+	if len(dir.Entries) == 0 {
+		return nil
+	}
+
+	best := dir.Entries[0]
+	for _, e := range dir.Entries {
+		if (e.width() > best.width()) && (e.height() > best.height()) {
+			best = e
+		}
+	}
+	return &best
+}
+
+// ParseIco parses the icon and returns meta information for the icons as icondir.
+func ParseIco(r io.Reader) (*icondir, error) {
+	dir := icondir{}
+
+	var err error
+	err = binary.Read(r, binary.LittleEndian, &dir.Reserved)
+	if err != nil {
+		return nil, err
+	}
+
+	err = binary.Read(r, binary.LittleEndian, &dir.Type)
+	if err != nil {
+		return nil, err
+	}
+
+	err = binary.Read(r, binary.LittleEndian, &dir.Count)
+	if err != nil {
+		return nil, err
+	}
+
+	for i := uint16(0); i < dir.Count; i++ {
+		entry := icondirEntry{}
+		e := parseIcondirEntry(r, &entry)
+		if e != nil {
+			return nil, e
+		}
+		dir.Entries = append(dir.Entries, entry)
+	}
+
+	return &dir, err
+}
+
+func parseIcondirEntry(r io.Reader, e *icondirEntry) error {
+	err := binary.Read(r, binary.LittleEndian, e)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+type dibHeader struct {
+	dibHeaderSize uint32
+	width         uint32
+	height        uint32
+}
+
+func (e *icondirEntry) ColorCount() int {
+	if e.PaletteCount == 0 {
+		return 256
+	}
+	return int(e.PaletteCount)
+}
+
+func (e *icondirEntry) width() int {
+	if e.Width == 0 {
+		return 256
+	}
+	return int(e.Width)
+}
+
+func (e *icondirEntry) height() int {
+	if e.Height == 0 {
+		return 256
+	}
+	return int(e.Height)
+}
+
+// DecodeConfig returns just the dimensions of the largest image
+// contained in the icon withou decoding the entire icon file.
+func DecodeConfig(r io.Reader) (image.Config, error) {
+	dir, err := ParseIco(r)
+	if err != nil {
+		return image.Config{}, err
+	}
+
+	best := dir.FindBestIcon()
+	if best == nil {
+		return image.Config{}, errInvalid
+	}
+	return image.Config{Width: best.width(), Height: best.height()}, nil
+}
+
+// The bitmap header structure we read from an icondirEntry
+type bitmapHeaderRead struct {
+	Size            uint32
+	Width           uint32
+	Height          uint32
+	Planes          uint16
+	BitCount        uint16
+	Compression     uint32
+	ImageSize       uint32
+	XPixelsPerMeter uint32
+	YPixelsPerMeter uint32
+	ColorsUsed      uint32
+	ColorsImportant uint32
+}
+
+// The bitmap header structure we need to generate for bmp.Decode()
+type bitmapHeaderWrite struct {
+	sigBM           [2]byte
+	fileSize        uint32
+	resverved       [2]uint16
+	pixOffset       uint32
+	Size            uint32
+	Width           uint32
+	Height          uint32
+	Planes          uint16
+	BitCount        uint16
+	Compression     uint32
+	ImageSize       uint32
+	XPixelsPerMeter uint32
+	YPixelsPerMeter uint32
+	ColorsUsed      uint32
+	ColorsImportant uint32
+}
+
+var errInvalid = errors.New("ico: invalid ICO image")
+
+// Decode returns the largest image contained in the icon
+// which might be a bmp or png
+func Decode(r io.Reader) (image.Image, error) {
+	icoBytes, err := ioutil.ReadAll(r)
+	if err != nil {
+		return nil, err
+	}
+
+	r = bytes.NewReader(icoBytes)
+	dir, err := ParseIco(r)
+	if err != nil {
+		return nil, errInvalid
+	}
+
+	best := dir.FindBestIcon()
+	if best == nil {
+		return nil, errInvalid
+	}
+
+	return parseImage(best, icoBytes)
+}
+
+func parseImage(entry *icondirEntry, icoBytes []byte) (image.Image, error) {
+	r := bytes.NewReader(icoBytes)
+	r.Seek(int64(entry.Offset), 0)
+
+	// Try PNG first then BMP
+	img, err := png.Decode(r)
+	if err != nil {
+		return parseBMP(entry, icoBytes)
+	}
+	return img, nil
+}
+
+func parseBMP(entry *icondirEntry, icoBytes []byte) (image.Image, error) {
+	bmpBytes, err := makeFullBMPBytes(entry, icoBytes)
+	if err != nil {
+		return nil, err
+	}
+	return bmp.Decode(bmpBytes)
+}
+
+func makeFullBMPBytes(entry *icondirEntry, icoBytes []byte) (*bytes.Buffer, error) {
+	r := bytes.NewReader(icoBytes)
+	r.Seek(int64(entry.Offset), 0)
+
+	var err error
+	h := bitmapHeaderRead{}
+
+	err = binary.Read(r, binary.LittleEndian, &h)
+	if err != nil {
+		return nil, err
+	}
+
+	if h.Size != 40 || h.Planes != 1 {
+		return nil, errInvalid
+	}
+
+	var pixOffset uint32
+	if h.ColorsUsed == 0 && h.BitCount <= 8 {
+		pixOffset = 14 + 40 + 4*(1<<h.BitCount)
+	} else {
+		pixOffset = 14 + 40 + 4*h.ColorsUsed
+	}
+
+	writeHeader := &bitmapHeaderWrite{
+		sigBM:           [2]byte{'B', 'M'},
+		fileSize:        14 + 40 + uint32(len(icoBytes)), // correct? important?
+		pixOffset:       pixOffset,
+		Size:            40,
+		Width:           uint32(h.Width),
+		Height:          uint32(h.Height / 2),
+		Planes:          h.Planes,
+		BitCount:        h.BitCount,
+		Compression:     h.Compression,
+		ColorsUsed:      h.ColorsUsed,
+		ColorsImportant: h.ColorsImportant,
+	}
+
+	buf := new(bytes.Buffer)
+	if err = binary.Write(buf, binary.LittleEndian, writeHeader); err != nil {
+		return nil, err
+	}
+	io.CopyN(buf, r, int64(entry.Size))
+
+	return buf, nil
+}
+
+const icoHeader = "\x00\x00\x01\x00"
+
+func init() {
+	image.RegisterFormat("ico", icoHeader, Decode, DecodeConfig)
+}

BIN
vendor/github.com/mat/besticon/ico/wowhead.ico


+ 92 - 0
vendor/github.com/mat/besticon/lettericon/fonts/LICENSE_OFL.txt

@@ -0,0 +1,92 @@
+This Font Software is licensed under the SIL Open Font License,
+Version 1.1.
+
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font
+creation efforts of academic and linguistic communities, and to
+provide a free and open framework in which fonts may be shared and
+improved in partnership with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply to
+any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software
+components as distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to,
+deleting, or substituting -- in part or in whole -- any of the
+components of the Original Version, by changing formats or by porting
+the Font Software to a new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed,
+modify, redistribute, and sell modified and unmodified copies of the
+Font Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components, in
+Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the
+corresponding Copyright Holder. This restriction only applies to the
+primary font name as presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created using
+the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.

+ 199 - 0
vendor/golang.org/x/image/bmp/reader.go

@@ -0,0 +1,199 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package bmp implements a BMP image decoder and encoder.
+//
+// The BMP specification is at http://www.digicamsoft.com/bmp/bmp.html.
+package bmp // import "golang.org/x/image/bmp"
+
+import (
+	"errors"
+	"image"
+	"image/color"
+	"io"
+)
+
+// ErrUnsupported means that the input BMP image uses a valid but unsupported
+// feature.
+var ErrUnsupported = errors.New("bmp: unsupported BMP image")
+
+func readUint16(b []byte) uint16 {
+	return uint16(b[0]) | uint16(b[1])<<8
+}
+
+func readUint32(b []byte) uint32 {
+	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
+}
+
+// decodePaletted reads an 8 bit-per-pixel BMP image from r.
+// If topDown is false, the image rows will be read bottom-up.
+func decodePaletted(r io.Reader, c image.Config, topDown bool) (image.Image, error) {
+	paletted := image.NewPaletted(image.Rect(0, 0, c.Width, c.Height), c.ColorModel.(color.Palette))
+	if c.Width == 0 || c.Height == 0 {
+		return paletted, nil
+	}
+	var tmp [4]byte
+	y0, y1, yDelta := c.Height-1, -1, -1
+	if topDown {
+		y0, y1, yDelta = 0, c.Height, +1
+	}
+	for y := y0; y != y1; y += yDelta {
+		p := paletted.Pix[y*paletted.Stride : y*paletted.Stride+c.Width]
+		if _, err := io.ReadFull(r, p); err != nil {
+			return nil, err
+		}
+		// Each row is 4-byte aligned.
+		if c.Width%4 != 0 {
+			_, err := io.ReadFull(r, tmp[:4-c.Width%4])
+			if err != nil {
+				return nil, err
+			}
+		}
+	}
+	return paletted, nil
+}
+
+// decodeRGB reads a 24 bit-per-pixel BMP image from r.
+// If topDown is false, the image rows will be read bottom-up.
+func decodeRGB(r io.Reader, c image.Config, topDown bool) (image.Image, error) {
+	rgba := image.NewRGBA(image.Rect(0, 0, c.Width, c.Height))
+	if c.Width == 0 || c.Height == 0 {
+		return rgba, nil
+	}
+	// There are 3 bytes per pixel, and each row is 4-byte aligned.
+	b := make([]byte, (3*c.Width+3)&^3)
+	y0, y1, yDelta := c.Height-1, -1, -1
+	if topDown {
+		y0, y1, yDelta = 0, c.Height, +1
+	}
+	for y := y0; y != y1; y += yDelta {
+		if _, err := io.ReadFull(r, b); err != nil {
+			return nil, err
+		}
+		p := rgba.Pix[y*rgba.Stride : y*rgba.Stride+c.Width*4]
+		for i, j := 0, 0; i < len(p); i, j = i+4, j+3 {
+			// BMP images are stored in BGR order rather than RGB order.
+			p[i+0] = b[j+2]
+			p[i+1] = b[j+1]
+			p[i+2] = b[j+0]
+			p[i+3] = 0xFF
+		}
+	}
+	return rgba, nil
+}
+
+// decodeNRGBA reads a 32 bit-per-pixel BMP image from r.
+// If topDown is false, the image rows will be read bottom-up.
+func decodeNRGBA(r io.Reader, c image.Config, topDown bool) (image.Image, error) {
+	rgba := image.NewNRGBA(image.Rect(0, 0, c.Width, c.Height))
+	if c.Width == 0 || c.Height == 0 {
+		return rgba, nil
+	}
+	y0, y1, yDelta := c.Height-1, -1, -1
+	if topDown {
+		y0, y1, yDelta = 0, c.Height, +1
+	}
+	for y := y0; y != y1; y += yDelta {
+		p := rgba.Pix[y*rgba.Stride : y*rgba.Stride+c.Width*4]
+		if _, err := io.ReadFull(r, p); err != nil {
+			return nil, err
+		}
+		for i := 0; i < len(p); i += 4 {
+			// BMP images are stored in BGRA order rather than RGBA order.
+			p[i+0], p[i+2] = p[i+2], p[i+0]
+		}
+	}
+	return rgba, nil
+}
+
+// Decode reads a BMP image from r and returns it as an image.Image.
+// Limitation: The file must be 8, 24 or 32 bits per pixel.
+func Decode(r io.Reader) (image.Image, error) {
+	c, bpp, topDown, err := decodeConfig(r)
+	if err != nil {
+		return nil, err
+	}
+	switch bpp {
+	case 8:
+		return decodePaletted(r, c, topDown)
+	case 24:
+		return decodeRGB(r, c, topDown)
+	case 32:
+		return decodeNRGBA(r, c, topDown)
+	}
+	panic("unreachable")
+}
+
+// DecodeConfig returns the color model and dimensions of a BMP image without
+// decoding the entire image.
+// Limitation: The file must be 8, 24 or 32 bits per pixel.
+func DecodeConfig(r io.Reader) (image.Config, error) {
+	config, _, _, err := decodeConfig(r)
+	return config, err
+}
+
+func decodeConfig(r io.Reader) (config image.Config, bitsPerPixel int, topDown bool, err error) {
+	// We only support those BMP images that are a BITMAPFILEHEADER
+	// immediately followed by a BITMAPINFOHEADER.
+	const (
+		fileHeaderLen = 14
+		infoHeaderLen = 40
+	)
+	var b [1024]byte
+	if _, err := io.ReadFull(r, b[:fileHeaderLen+infoHeaderLen]); err != nil {
+		return image.Config{}, 0, false, err
+	}
+	if string(b[:2]) != "BM" {
+		return image.Config{}, 0, false, errors.New("bmp: invalid format")
+	}
+	offset := readUint32(b[10:14])
+	if readUint32(b[14:18]) != infoHeaderLen {
+		return image.Config{}, 0, false, ErrUnsupported
+	}
+	width := int(int32(readUint32(b[18:22])))
+	height := int(int32(readUint32(b[22:26])))
+	if height < 0 {
+		height, topDown = -height, true
+	}
+	if width < 0 || height < 0 {
+		return image.Config{}, 0, false, ErrUnsupported
+	}
+	// We only support 1 plane, 8 or 24 bits per pixel and no compression.
+	planes, bpp, compression := readUint16(b[26:28]), readUint16(b[28:30]), readUint32(b[30:34])
+	if planes != 1 || compression != 0 {
+		return image.Config{}, 0, false, ErrUnsupported
+	}
+	switch bpp {
+	case 8:
+		if offset != fileHeaderLen+infoHeaderLen+256*4 {
+			return image.Config{}, 0, false, ErrUnsupported
+		}
+		_, err = io.ReadFull(r, b[:256*4])
+		if err != nil {
+			return image.Config{}, 0, false, err
+		}
+		pcm := make(color.Palette, 256)
+		for i := range pcm {
+			// BMP images are stored in BGR order rather than RGB order.
+			// Every 4th byte is padding.
+			pcm[i] = color.RGBA{b[4*i+2], b[4*i+1], b[4*i+0], 0xFF}
+		}
+		return image.Config{ColorModel: pcm, Width: width, Height: height}, 8, topDown, nil
+	case 24:
+		if offset != fileHeaderLen+infoHeaderLen {
+			return image.Config{}, 0, false, ErrUnsupported
+		}
+		return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 24, topDown, nil
+	case 32:
+		if offset != fileHeaderLen+infoHeaderLen {
+			return image.Config{}, 0, false, ErrUnsupported
+		}
+		return image.Config{ColorModel: color.RGBAModel, Width: width, Height: height}, 32, topDown, nil
+	}
+	return image.Config{}, 0, false, ErrUnsupported
+}
+
+func init() {
+	image.RegisterFormat("bmp", "BM????\x00\x00\x00\x00", Decode, DecodeConfig)
+}

+ 166 - 0
vendor/golang.org/x/image/bmp/writer.go

@@ -0,0 +1,166 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package bmp
+
+import (
+	"encoding/binary"
+	"errors"
+	"image"
+	"io"
+)
+
+type header struct {
+	sigBM           [2]byte
+	fileSize        uint32
+	resverved       [2]uint16
+	pixOffset       uint32
+	dibHeaderSize   uint32
+	width           uint32
+	height          uint32
+	colorPlane      uint16
+	bpp             uint16
+	compression     uint32
+	imageSize       uint32
+	xPixelsPerMeter uint32
+	yPixelsPerMeter uint32
+	colorUse        uint32
+	colorImportant  uint32
+}
+
+func encodePaletted(w io.Writer, pix []uint8, dx, dy, stride, step int) error {
+	var padding []byte
+	if dx < step {
+		padding = make([]byte, step-dx)
+	}
+	for y := dy - 1; y >= 0; y-- {
+		min := y*stride + 0
+		max := y*stride + dx
+		if _, err := w.Write(pix[min:max]); err != nil {
+			return err
+		}
+		if padding != nil {
+			if _, err := w.Write(padding); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func encodeRGBA(w io.Writer, pix []uint8, dx, dy, stride, step int) error {
+	buf := make([]byte, step)
+	for y := dy - 1; y >= 0; y-- {
+		min := y*stride + 0
+		max := y*stride + dx*4
+		off := 0
+		for i := min; i < max; i += 4 {
+			buf[off+2] = pix[i+0]
+			buf[off+1] = pix[i+1]
+			buf[off+0] = pix[i+2]
+			off += 3
+		}
+		if _, err := w.Write(buf); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func encode(w io.Writer, m image.Image, step int) error {
+	b := m.Bounds()
+	buf := make([]byte, step)
+	for y := b.Max.Y - 1; y >= b.Min.Y; y-- {
+		off := 0
+		for x := b.Min.X; x < b.Max.X; x++ {
+			r, g, b, _ := m.At(x, y).RGBA()
+			buf[off+2] = byte(r >> 8)
+			buf[off+1] = byte(g >> 8)
+			buf[off+0] = byte(b >> 8)
+			off += 3
+		}
+		if _, err := w.Write(buf); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Encode writes the image m to w in BMP format.
+func Encode(w io.Writer, m image.Image) error {
+	d := m.Bounds().Size()
+	if d.X < 0 || d.Y < 0 {
+		return errors.New("bmp: negative bounds")
+	}
+	h := &header{
+		sigBM:         [2]byte{'B', 'M'},
+		fileSize:      14 + 40,
+		pixOffset:     14 + 40,
+		dibHeaderSize: 40,
+		width:         uint32(d.X),
+		height:        uint32(d.Y),
+		colorPlane:    1,
+	}
+
+	var step int
+	var palette []byte
+	switch m := m.(type) {
+	case *image.Gray:
+		step = (d.X + 3) &^ 3
+		palette = make([]byte, 1024)
+		for i := 0; i < 256; i++ {
+			palette[i*4+0] = uint8(i)
+			palette[i*4+1] = uint8(i)
+			palette[i*4+2] = uint8(i)
+			palette[i*4+3] = 0xFF
+		}
+		h.imageSize = uint32(d.Y * step)
+		h.fileSize += uint32(len(palette)) + h.imageSize
+		h.pixOffset += uint32(len(palette))
+		h.bpp = 8
+
+	case *image.Paletted:
+		step = (d.X + 3) &^ 3
+		palette = make([]byte, 1024)
+		for i := 0; i < len(m.Palette) && i < 256; i++ {
+			r, g, b, _ := m.Palette[i].RGBA()
+			palette[i*4+0] = uint8(b >> 8)
+			palette[i*4+1] = uint8(g >> 8)
+			palette[i*4+2] = uint8(r >> 8)
+			palette[i*4+3] = 0xFF
+		}
+		h.imageSize = uint32(d.Y * step)
+		h.fileSize += uint32(len(palette)) + h.imageSize
+		h.pixOffset += uint32(len(palette))
+		h.bpp = 8
+	default:
+		step = (3*d.X + 3) &^ 3
+		h.imageSize = uint32(d.Y * step)
+		h.fileSize += h.imageSize
+		h.bpp = 24
+	}
+
+	if err := binary.Write(w, binary.LittleEndian, h); err != nil {
+		return err
+	}
+	if palette != nil {
+		if err := binary.Write(w, binary.LittleEndian, palette); err != nil {
+			return err
+		}
+	}
+
+	if d.X == 0 || d.Y == 0 {
+		return nil
+	}
+
+	switch m := m.(type) {
+	case *image.Gray:
+		return encodePaletted(w, m.Pix, d.X, d.Y, m.Stride, step)
+	case *image.Paletted:
+		return encodePaletted(w, m.Pix, d.X, d.Y, m.Stride, step)
+	case *image.RGBA:
+		return encodeRGBA(w, m.Pix, d.X, d.Y, m.Stride, step)
+	}
+	return encode(w, m, step)
+}

+ 13 - 0
vips.h

@@ -69,6 +69,9 @@ vips_type_find_save_go(int imgtype) {
   if (imgtype == GIF) {
     return vips_type_find("VipsOperation", "magicksave_buffer");
   }
+  if (imgtype == ICO) {
+    return vips_type_find("VipsOperation", "magicksave_buffer");
+  }
   return 0;
 }
 
@@ -287,6 +290,16 @@ vips_gifsave_go(VipsImage *in, void **buf, size_t *len) {
 #endif
 }
 
+int
+vips_icosave_go(VipsImage *in, void **buf, size_t *len) {
+#if VIPS_SUPPORT_MAGICK
+  return vips_magicksave_buffer(in, buf, len, "format", "ico", NULL);
+#else
+  vips_error("vips_icosave_go", "Saving ICO is not supported");
+  return 1;
+#endif
+}
+
 void
 vips_cleanup() {
   vips_thread_shutdown();