#はじめに
前回の記事の「Slackのemojiをドラッグ&ドロップで追加できるアプリをGolangで作った」で、ようやくアニメーションGif(以下アニメGif)のリサイズが出来たのでメモがてら記事を書きます。
上記のアプリを作った当初はgifのリサイズを行うと色がおかしくなってしまう問題があって、gifの対応を見送りました。
落ち着いて調べ直したら色々と分かったことがあったので、お役に立てば幸いです。
#サンプル
ネットで探してきた素材の縮小例です。
http://droid-chan.net/archives/338
小さくなった分荒くはなりますが、リサイズ出来てちゃんと動いています。
#概要
今回は以下のライブラリを使用してリサイズします。
nfnt/resize
処理の流れは以下の順序で行います。
- アニメGifを読み込む
- 現在のgifのサイズを取得
- リサイズする比を計算
- アニメGif内のgif画像を1枚ずつ縮小
- アニメGifをファイルに出力
こう書くと単純ですが、4の工程が中々難しかったです。
#コード
リサイズに使用した関数を転記します。
一部説明の都合上、処理を入れ替えている箇所があります。
実際に使用したソースなので、jpgやpngのリサイズの処理も入っています。
func resizeImage(filePath string, maxSize float64) error {
const postFix string = "_resized"
base := filepath.Base(filePath)
ext := filepath.Ext(filePath)
ext = strings.ToLower(ext)
imageFile, err := os.Open(filePath)
if err != nil {
log.Fatal(err)
}
defer imageFile.Close()
var decImage image.Image
var gifImage *gif.GIF
var imageConfig image.Config
if ext == ".jpg" || ext == ".jpeg" {
decImage, err = jpeg.Decode(imageFile)
if err != nil {
log.Println(err)
}
_, err = imageFile.Seek(io.SeekStart, 0)
if err != nil {
return err
}
imageConfig, err = jpeg.DecodeConfig(imageFile)
if err != nil {
return err
}
} else if ext == ".png" {
decImage, err = png.Decode(imageFile)
if err != nil {
return err
}
_, err = imageFile.Seek(io.SeekStart, 0)
if err != nil {
return err
}
imageConfig, err = png.DecodeConfig(imageFile)
if err != nil {
return err
}
} else if ext == ".gif" {
gifImage, err = gif.DecodeAll(imageFile)
if err != nil {
return err
}
imageConfig = gifImage.Config
if err != nil {
return err
}
} else {
return nil
}
width := float64(imageConfig.Width)
height := float64(imageConfig.Height)
var ratio float64
if width > height && width > maxSize {
ratio = maxSize / width
} else if height > maxSize {
ratio = maxSize / height
} else {
ratio = 1
}
tmpFileName := base[0:len(base)-len(ext)] + postFix + ext
tmpFile, err := os.Create(tmpFileName)
if err != nil {
return err
}
defer tmpFile.Close()
if ratio == 1 {
_, err = imageFile.Seek(io.SeekStart, 0)
if err != nil {
return err
}
_, err := io.Copy(tmpFile, imageFile)
if err != nil {
log.Fatal(err)
return err
}
} else {
if ext == ".jpg" || ext == ".jpeg" {
resized := resize.Resize(uint(math.Floor(width*ratio)), uint(math.Floor(height*ratio)),
decImage, resize.Lanczos3)
jpeg.Encode(tmpFile, resized, nil)
} else if ext == ".png" {
resized := resize.Resize(uint(math.Floor(width*ratio)), uint(math.Floor(height*ratio)),
decImage, resize.Lanczos3)
png.Encode(tmpFile, resized)
} else if ext == ".gif" {
for index, frame := range gifImage.Image {
rect := frame.Bounds()
tmpImage := frame.SubImage(rect)
resizedImage := resize.Resize(uint(math.Floor(float64(rect.Dx())*ratio)),
uint(math.Floor(float64(rect.Dy())*ratio)),
tmpImage, resize.Lanczos3)
// Add colors from original gif image
var tmpPalette color.Palette
for x := 1; x <= rect.Dx(); x++ {
for y := 1; y <= rect.Dy(); y++ {
if !contains(tmpPalette, gifImage.Image[index].At(x, y)) {
tmpPalette = append(tmpPalette, gifImage.Image[index].At(x, y))
}
}
}
// After first image, image may contains only difference
// bounds may not start from at (0,0)
resizedBounds := resizedImage.Bounds()
if index >= 1 {
marginX := int(math.Floor(float64(rect.Min.X) * ratio))
marginY := int(math.Floor(float64(rect.Min.Y) * ratio))
resizedBounds = image.Rect(marginX, marginY, resizedBounds.Dx()+marginX,
resizedBounds.Dy()+marginY)
}
resizedPalette := image.NewPaletted(resizedBounds, tmpPalette)
draw.Draw(resizedPalette, resizedBounds, resizedImage, image.ZP, draw.Src)
gifImage.Image[index] = resizedPalette
}
// Set size to resized size
gifImage.Config.Width = int(math.Floor(width * ratio))
gifImage.Config.Height = int(math.Floor(height * ratio))
gif.EncodeAll(tmpFile, gifImage)
}
}
return nil
}
// Check if color is already in the Palette
func contains(colorPalette color.Palette, c color.Color) bool {
for _, tmpColor := range colorPalette {
if tmpColor == c {
return true
}
}
return false
}
#ポイント
アニメGifをリサイズするにあたってポイントを幾つか解説します。
##アニメGifを読み込む時
gif.DecodeAllを使用します。
var gifImage *gif.GIF
gifImage, err = gif.DecodeAll(imageFile)
他のjpgやpngと違い難しいのは、DecodeAll
から返却される値が*GIFというところです。
Decode
から返却されるのはImage
なのでそのままresize.Resize
に渡せるのですが、GIF
にはImage
がありません。
参考:
type GIF struct {
Image []*image.Paletted // The successive images.
Delay []int // The successive delay times, one per frame, in 100ths of a second.
LoopCount int // The loop count.
Disposal []byte
BackgroundIndex byte
}
そのため、ここからImage
を作る必要があります。
##各画像を縮小する
先程取得したGIF
はアニメGifの画像全てが格納されているため、range
を使用してループで回します。
for index, frame := range gifImage.Image {
GIF
内のImage
をrange
で回すことで、index
とPalettedを取得できます。
Paletted
はSubImageを使って、Image
に変換できます。
Paletted
内のRectangle
を使用して、原寸大のImage
を作ります。
rect := frame.Bounds()
tmpImage := frame.SubImage(rect)
あとはこのImage
をResize
に渡して、リサイズすればいいだけ・・・と思っていたらそう簡単ではありませんでした。
##ImageをPalettedに変換する
GIF
はImage
ではなく、Paletted
で画像を保持しています。
そのため、リサイズしたImage
をPaletted
に戻さなければいけません。
NewPalettedという関数を使用すれば出来そうです。
func NewPaletted(r Rectangle, p color.Palette) *Paletted
そのためにはRectangle
とPalette
を作る必要があります。
まずはPalette
を作ります。
###Paletteを作成
Gifは256色しか使用できませんが、リサイズ直後の画像は256色を超えている可能性があります。
(このことに中々気付けませんでした・・・。)
当初別のソースを参考にしてpalette.Plan9
をPalette
として使用しましたが、色がおかしくなってしまいました。
元画像が持っていた色と違うから当たり前ですね・・・。
試行錯誤した結果、元画像から色情報を取得するという結論に至りました。
// Add colors from original gif image
var tmpPalette color.Palette
for x := 1; x <= rect.Dx(); x++ {
for y := 1; y <= rect.Dy(); y++ {
if !contains(tmpPalette, gifImage.Image[index].At(x, y)) {
tmpPalette = append(tmpPalette, gifImage.Image[index].At(x, y))
}
}
}
読み込んだ元画像をループで回し、まだ追加していないColor
があったらPalette
に追加するという処理です。
At
関数でPaletted
の特定のポイントのColor
情報を取得できます。
これで元画像の色情報を持ったPalette
が出来ました。
###Rectangleを作成
ここも一筋縄ではいきませんでした・・・。
アニメGifの1枚目以降はフレームの再利用が可能なため、差分しか情報がない場合があります。
つまり、座標が(0,0)から始まらない場合があります。
しかし、Resize
はこのことを考慮してくれず、描画開始位置が(0,0)に戻ってしまうため、描画開始位置を適切な箇所にずらしてあげなければいけません。
// After first image, image may contains only difference
// bounds may not start from at (0,0)
resizedBounds := resizedImage.Bounds()
if index >= 1 {
marginX := int(math.Floor(float64(rect.Min.X) * ratio))
marginY := int(math.Floor(float64(rect.Min.Y) * ratio))
resizedBounds = image.Rect(marginX, marginY, resizedBounds.Dx()+marginX,
resizedBounds.Dy()+marginY)
}
元画像のMin.X
とMin.Y
の位置を取得して縮小し、リサイズ後のRectangle
に反映します。
これでRectangle
とPalette
が出来ました。
##リサイズした画像を描画
NewPaletted
を使用してPaletted
を作成後、リサイズした画像をDraw
を使用して描画します。
描画したPaletted
を元画像と差し替えます。
resizedPalette := image.NewPaletted(resizedBounds, tmpPalette)
draw.Draw(resizedPalette, resizedBounds, resizedImage, image.ZP, draw.Src)
gifImage.Image[index] = resizedPalette
##アニメGifを出力
アニメGifを出力する前に、Config
のサイズをリサイズ後のサイズに変更します。
その後、gif.EncodeAllでファイルを出力します。
// Set size to resized size
gifImage.Config.Width = int(math.Floor(width * ratio))
gifImage.Config.Height = int(math.Floor(height * ratio))
gif.EncodeAll(tmpFile, gifImage)
これでリサイズされたアニメGifが出力されます。
#作ってみて
最初はどうしたものかと思いましたが、どんなデータが入っているのかとにかく出力してみたらなんとなく解決の糸口が見つかりました。
もし簡単に出来る方法がありましたら、ぜひ教えて下さい。
今回の経験を応用して、アニメGifで色々やってみたいと思います。