From 4e5fbdd91efbdbae1e3491ff1c93ecc99c0e2c2a Mon Sep 17 00:00:00 2001 From: Erik Unger Date: Mon, 2 Jul 2012 10:21:32 +0200 Subject: [PATCH] media image resize --- media/functions.go | 2 +- media/image.go | 86 ++++++++++++++--------- media/imageversion.go | 56 ++++++++++++++- media/resize.go | 156 ++++++++++++++++++++++++++++++++--------- media/resize1.go | 158 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 392 insertions(+), 66 deletions(-) create mode 100644 media/resize1.go diff --git a/media/functions.go b/media/functions.go index 5e2f605..0abdcb2 100644 --- a/media/functions.go +++ b/media/functions.go @@ -14,7 +14,7 @@ func ValidUrlFilename(filename string) string { } else if c >= 'A' && c <= 'Z' { result[i] = byte(unicode.ToLower(c)) } else { - result[i] = '_' + result[i] = '~' } i++ } diff --git a/media/image.go b/media/image.go index 1fd7395..773e2c0 100644 --- a/media/image.go +++ b/media/image.go @@ -1,8 +1,6 @@ package media import ( - "github.com/ungerik/go-start/model" - // "github.com/ungerik/go-start/view" "bytes" "image" "image/png" @@ -10,12 +8,14 @@ import ( _ "code.google.com/p/go.image/tiff" _ "code.google.com/p/go.image/bmp" "image/color" + "github.com/ungerik/go-start/model" + // "github.com/ungerik/go-start/view" ) // NewImage creates a new Image and saves the original version to Config.Backend. // GIF, TIFF, BMP images will be read, but written as PNG. -func NewImage(file model.File) (*Image, error) { - i, t, err := image.Decode(bytes.NewReader(file.Data)) +func NewImage(filename string, data []byte) (*Image, error) { + i, t, err := image.Decode(bytes.NewReader(data)) if err != nil { return nil, err } @@ -25,23 +25,23 @@ func NewImage(file model.File) (*Image, error) { if err != nil { return nil, err } - file.Data = buf.Bytes() - file.Name += ".png" - i, t, err = image.Decode(bytes.NewReader(file.Data)) + data = buf.Bytes() + filename += ".png" + i, t, err = image.Decode(bytes.NewReader(data)) if err != nil { return nil, err } } result := &Image{ Versions: []ImageVersion{{ - Filename: model.String(ValidUrlFilename(file.Name)), + Filename: model.String(ValidUrlFilename(filename)), ContentType: model.String("image/" + t), Width: model.Int(i.Bounds().Dx()), Height: model.Int(i.Bounds().Dy()), Grayscale: model.Bool(i.ColorModel() == color.GrayModel || i.ColorModel() == color.Gray16Model), }}, } - err = result.Versions[0].SaveData(file.Data) + err = result.Versions[0].SaveImageData(data) if err != nil { return nil, err } @@ -75,33 +75,38 @@ func (self *Image) Grayscale() bool { return self.Versions[0].Grayscale.Get() } -func (self *Image) outerSizeWithOriginalAspectRatio(width, height int) (int, int) { - originalAspectRatio := float32(self.Width()) / float32(self.Height()) - aspectRatio := float32(width) / float32(height) +// AspectRatio returns Width / Height +func (self *Image) AspectRatio() float64 { + return self.Versions[0].AspectRatio() +} + +func (self *Image) touchFromOutsideWithOriginalAspectRatio(width, height int) (int, int) { + aspectRatio := float64(width) / float64(height) + originalAspectRatio := self.AspectRatio() if aspectRatio > originalAspectRatio { // Wider than original - return width, int(float32(width) / originalAspectRatio) + return width, int(float64(width) / originalAspectRatio) } // Heigher than original - return int(float32(height) * originalAspectRatio), height + return int(float64(height) * originalAspectRatio), height } -func (self *Image) newVersion(width, height int, grayscale bool) (*ImageVersion, error) { - image, err := self.Versions[0].LoadImage() - if err != nil { - return nil, err - } - if image == nil { - } - version := &ImageVersion{ - Filename: self.Versions[0].Filename, - ContentType: self.Versions[0].ContentType, - Width: model.Int(width), - Height: model.Int(height), - Grayscale: model.Bool(grayscale), - } - return version, nil -} +// func (self *Image) newVersion(width, height int, grayscale bool) (*ImageVersion, error) { +// image, err := self.Versions[0].LoadImage() +// if err != nil { +// return nil, err +// } +// if image == nil { +// } +// version := &ImageVersion{ +// Filename: self.Versions[0].Filename, +// ContentType: self.Versions[0].ContentType, +// Width: model.Int(width), +// Height: model.Int(height), +// Grayscale: model.Bool(grayscale), +// } +// return version, nil +// } func (self *Image) Version(width, height int, grayscale bool) (*ImageVersion, error) { if self.Grayscale() { @@ -109,9 +114,11 @@ func (self *Image) Version(width, height int, grayscale bool) (*ImageVersion, er grayscale = true } + aspectRatio := float64(width) / float64(height) + // If requested image is larger than original size, return original if width > self.Width() || height > self.Height() { - + // todo } // Search for exact match @@ -123,5 +130,22 @@ func (self *Image) Version(width, height int, grayscale bool) (*ImageVersion, er } // + outerWidth, outerHeight := self.touchFromOutsideWithOriginalAspectRatio(width, height) + orig, err := self.Versions[0].LoadImage() + if err != nil { + return nil, err + } + var r image.Rectangle + scaled := ResizeImage(orig, r, width, height) + + version := &ImageVersion{ + Filename: self.Versions[0].Filename, + ContentType: self.Versions[0].ContentType, + Width: model.Int(width), + Height: model.Int(height), + Grayscale: model.Bool(grayscale), + } + self.Versions = append(self.Versions, *version) + return nil, nil } diff --git a/media/imageversion.go b/media/imageversion.go index 5a0d9b0..92f2a37 100644 --- a/media/imageversion.go +++ b/media/imageversion.go @@ -2,6 +2,9 @@ package media import ( "image" + "image/png" + "image/jpeg" + "errors" "github.com/ungerik/go-start/model" ) @@ -18,10 +21,57 @@ func (self *ImageVersion) URL() string { return View.URL(self.ID.Get(), self.Filename.Get()) } -func (self *ImageVersion) SaveData(data []byte) error { - return nil +// AspectRatio returns Width / Height +func (self *ImageVersion) AspectRatio() float64 { + return float64(self.Width) / float64(self.Height) +} + +func (self *ImageVersion) SaveImageData(data []byte) error { + writer, err := Config.Backend.ImageVersionWriter(self) + if err != nil { + return err + } + _, err = writer.Write(data) + if err != nil { + writer.Close() + return err + } + return writer.Close() +} + +func (self *ImageVersion) SaveImage(im image.Image) error { + writer, err := Config.Backend.ImageVersionWriter(self) + if err != nil { + return err + } + switch self.ContentType { + case "image/jpeg": + err = jpeg.Encode(writer, im, nil) + case "image/png": + err = png.Encode(writer, im) + default: + return errors.New("Can't save content-type: " + self.ContentType.Get()) + } + if err != nil { + writer.Close() + return err + } + return writer.Close() } func (self *ImageVersion) LoadImage() (image.Image, error) { - return nil, nil + reader, _, err := Config.Backend.ImageVersionReader(self.ID.Get()) + if err != nil { + return nil, err + } + im, _, err := image.Decode(reader) + if err != nil { + reader.Close() + return nil, err + } + err = reader.Close() + if err != nil { + return nil, err + } + return im, nil } diff --git a/media/resize.go b/media/resize.go index 3ee507b..227e32d 100644 --- a/media/resize.go +++ b/media/resize.go @@ -9,47 +9,144 @@ import ( "image/color" ) +// Resize returns a scaled copy of the image slice r of m. +// The returned image has width w and height h. +func ResizeImage(m image.Image, r image.Rectangle, w, h int) image.Image { + if w < 0 || h < 0 { + panic("Negative image size") + } + if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { + return image.NewRGBA64(image.Rect(0, 0, w, h)) + } + switch m := m.(type) { + case *image.RGBA: + return resizeRGBA(m, r, w, h) + case *image.YCbCr: + if m, ok := resizeYCbCr(m, r, w, h); ok { + return m + } + } + ww, hh := uint64(w), uint64(h) + dx, dy := uint64(r.Dx()), uint64(r.Dy()) + // The scaling algorithm is to nearest-neighbor magnify the dx * dy source + // to a (ww*dx) * (hh*dy) intermediate image and then minify the intermediate + // image back down to a ww * hh destination with a simple box filter. + // The intermediate image is implied, we do not physically allocate a slice + // of length ww*dx*hh*dy. + // For example, consider a 4*3 source image. Label its pixels from a-l: + // abcd + // efgh + // ijkl + // To resize this to a 3*2 destination image, the intermediate is 12*6. + // Whitespace has been added to delineate the destination pixels: + // aaab bbcc cddd + // aaab bbcc cddd + // eeef ffgg ghhh + // + // eeef ffgg ghhh + // iiij jjkk klll + // iiij jjkk klll + // Thus, the 'b' source pixel contributes one third of its value to the + // (0, 0) destination pixel and two thirds to (1, 0). + // The implementation is a two-step process. First, the source pixels are + // iterated over and each source pixel's contribution to 1 or more + // destination pixels are summed. Second, the sums are divided by a scaling + // factor to yield the destination pixels. + // TODO: By interleaving the two steps, instead of doing all of + // step 1 first and all of step 2 second, we could allocate a smaller sum + // slice of length 4*w*2 instead of 4*w*h, although the resultant code + // would become more complicated. + n, sum := dx*dy, make([]uint64, 4*w*h) + for y := r.Min.Y; y < r.Max.Y; y++ { + for x := r.Min.X; x < r.Max.X; x++ { + // Get the source pixel. + r32, g32, b32, a32 := m.At(x, y).RGBA() + r64 := uint64(r32) + g64 := uint64(g32) + b64 := uint64(b32) + a64 := uint64(a32) + // Spread the source pixel over 1 or more destination rows. + py := uint64(y-r.Min.Y) * hh + for remy := hh; remy > 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x-r.Min.X) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + sum[index+0] += r64 * qx * qy + sum[index+1] += g64 * qx * qy + sum[index+2] += b64 * qx * qy + sum[index+3] += a64 * qx * qy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, n*0x0101) +} + // average convert the sums to averages and returns the result. -func average(sum []uint64, w, h int, n uint64) *image.RGBA { +func average(sum []uint64, w, h int, n uint64) image.Image { ret := image.NewRGBA(image.Rect(0, 0, w, h)) for y := 0; y < h; y++ { for x := 0; x < w; x++ { - index := 4 * (y*w + x) - pix := ret.Pix[y*ret.Stride+x*4:] - pix[0] = uint8(sum[index+0] / n) - pix[1] = uint8(sum[index+1] / n) - pix[2] = uint8(sum[index+2] / n) - pix[3] = uint8(sum[index+3] / n) + i := y*ret.Stride + x*4 + j := 4 * (y*w + x) + ret.Pix[i+0] = uint8(sum[j+0] / n) + ret.Pix[i+1] = uint8(sum[j+1] / n) + ret.Pix[i+2] = uint8(sum[j+2] / n) + ret.Pix[i+3] = uint8(sum[j+3] / n) } } return ret } -// ResizeRGBA returns a scaled copy of the RGBA image slice r of m. +// resizeYCbCr returns a scaled copy of the YCbCr image slice r of m. // The returned image has width w and height h. -func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA { +func resizeYCbCr(m *image.YCbCr, r image.Rectangle, w, h int) (image.Image, bool) { + var verticalRes int + switch m.SubsampleRatio { + case image.YCbCrSubsampleRatio420: + verticalRes = 2 + case image.YCbCrSubsampleRatio422: + verticalRes = 1 + default: + return nil, false + } ww, hh := uint64(w), uint64(h) dx, dy := uint64(r.Dx()), uint64(r.Dy()) // See comment in Resize. n, sum := dx*dy, make([]uint64, 4*w*h) for y := r.Min.Y; y < r.Max.Y; y++ { - pix := m.Pix[(y-r.Min.Y)*m.Stride:] + Y := m.Y[y*m.YStride:] + Cb := m.Cb[y/verticalRes*m.CStride:] + Cr := m.Cr[y/verticalRes*m.CStride:] for x := r.Min.X; x < r.Max.X; x++ { // Get the source pixel. - p := pix[(x-r.Min.X)*4:] - r64 := uint64(p[0]) - g64 := uint64(p[1]) - b64 := uint64(p[2]) - a64 := uint64(p[3]) + r8, g8, b8 := color.YCbCrToRGB(Y[x], Cb[x/2], Cr[x/2]) + r64 := uint64(r8) + g64 := uint64(g8) + b64 := uint64(b8) // Spread the source pixel over 1 or more destination rows. - py := uint64(y) * hh + py := uint64(y-r.Min.Y) * hh for remy := hh; remy > 0; { qy := dy - (py % dy) if qy > remy { qy = remy } // Spread the source pixel over 1 or more destination columns. - px := uint64(x) * ww + px := uint64(x-r.Min.X) * ww index := 4 * ((py/dy)*ww + (px / dx)) for remx := ww; remx > 0; { qx := dx - (px % dx) @@ -60,7 +157,7 @@ func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA { sum[index+0] += r64 * qxy sum[index+1] += g64 * qxy sum[index+2] += b64 * qxy - sum[index+3] += a64 * qxy + sum[index+3] += 0xFFFF * qxy index += 4 px += qx remx -= qx @@ -70,37 +167,34 @@ func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA { } } } - return average(sum, w, h, n) + return average(sum, w, h, n), true } -// ResizeNRGBA returns a scaled copy of the RGBA image slice r of m. +// resizeRGBA returns a scaled copy of the RGBA image slice r of m. // The returned image has width w and height h. -func ResizeNRGBA(m *image.NRGBA, r image.Rectangle, w, h int) *image.RGBA { +func resizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) image.Image { ww, hh := uint64(w), uint64(h) dx, dy := uint64(r.Dx()), uint64(r.Dy()) // See comment in Resize. n, sum := dx*dy, make([]uint64, 4*w*h) for y := r.Min.Y; y < r.Max.Y; y++ { - pix := m.Pix[(y-r.Min.Y)*m.Stride:] + pix := m.Pix[(y-m.Rect.Min.Y)*m.Stride:] for x := r.Min.X; x < r.Max.X; x++ { // Get the source pixel. - p := pix[(x-r.Min.X)*4:] + p := pix[(x-m.Rect.Min.X)*4:] r64 := uint64(p[0]) g64 := uint64(p[1]) b64 := uint64(p[2]) a64 := uint64(p[3]) - r64 = (r64 * a64) / 255 - g64 = (g64 * a64) / 255 - b64 = (b64 * a64) / 255 // Spread the source pixel over 1 or more destination rows. - py := uint64(y) * hh + py := uint64(y-r.Min.Y) * hh for remy := hh; remy > 0; { qy := dy - (py % dy) if qy > remy { qy = remy } // Spread the source pixel over 1 or more destination columns. - px := uint64(x) * ww + px := uint64(x-r.Min.X) * ww index := 4 * ((py/dy)*ww + (px / dx)) for remx := ww; remx > 0; { qx := dx - (px % dx) @@ -126,12 +220,12 @@ func ResizeNRGBA(m *image.NRGBA, r image.Rectangle, w, h int) *image.RGBA { // Resample returns a resampled copy of the image slice r of m. // The returned image has width w and height h. -func Resample(m image.Image, r image.Rectangle, w, h int) *image.RGBA { +func ResampleImage(m image.Image, r image.Rectangle, w, h int) image.Image { if w < 0 || h < 0 { - return nil + panic("Negative image size") } if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { - return image.NewRGBA(image.Rect(0, 0, w, h)) + return image.NewRGBA64(image.Rect(0, 0, w, h)) } curw, curh := r.Dx(), r.Dy() img := image.NewRGBA(image.Rect(0, 0, w, h)) diff --git a/media/resize1.go b/media/resize1.go new file mode 100644 index 0000000..14be656 --- /dev/null +++ b/media/resize1.go @@ -0,0 +1,158 @@ +// 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 media + +// import ( +// "image" +// "image/color" +// ) + +// // average convert the sums to averages and returns the result. +// func average(sum []uint64, w, h int, n uint64) *image.RGBA { +// ret := image.NewRGBA(image.Rect(0, 0, w, h)) +// for y := 0; y < h; y++ { +// for x := 0; x < w; x++ { +// index := 4 * (y*w + x) +// pix := ret.Pix[y*ret.Stride+x*4:] +// pix[0] = uint8(sum[index+0] / n) +// pix[1] = uint8(sum[index+1] / n) +// pix[2] = uint8(sum[index+2] / n) +// pix[3] = uint8(sum[index+3] / n) +// } +// } +// return ret +// } + +// // ResizeRGBA returns a scaled copy of the RGBA image slice r of m. +// // The returned image has width w and height h. +// func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA { +// if w < 0 || h < 0 { +// panic("Negative image size") +// } +// ww, hh := uint64(w), uint64(h) +// dx, dy := uint64(r.Dx()), uint64(r.Dy()) +// // See comment in Resize. +// n, sum := dx*dy, make([]uint64, 4*w*h) +// for y := r.Min.Y; y < r.Max.Y; y++ { +// pix := m.Pix[(y-r.Min.Y)*m.Stride:] +// for x := r.Min.X; x < r.Max.X; x++ { +// // Get the source pixel. +// p := pix[(x-r.Min.X)*4:] +// r64 := uint64(p[0]) +// g64 := uint64(p[1]) +// b64 := uint64(p[2]) +// a64 := uint64(p[3]) +// // Spread the source pixel over 1 or more destination rows. +// py := uint64(y) * hh +// for remy := hh; remy > 0; { +// qy := dy - (py % dy) +// if qy > remy { +// qy = remy +// } +// // Spread the source pixel over 1 or more destination columns. +// px := uint64(x) * ww +// index := 4 * ((py/dy)*ww + (px / dx)) +// for remx := ww; remx > 0; { +// qx := dx - (px % dx) +// if qx > remx { +// qx = remx +// } +// qxy := qx * qy +// sum[index+0] += r64 * qxy +// sum[index+1] += g64 * qxy +// sum[index+2] += b64 * qxy +// sum[index+3] += a64 * qxy +// index += 4 +// px += qx +// remx -= qx +// } +// py += qy +// remy -= qy +// } +// } +// } +// return average(sum, w, h, n) +// } + +// // ResizeNRGBA returns a scaled copy of the RGBA image slice r of m. +// // The returned image has width w and height h. +// func ResizeNRGBA(m *image.NRGBA, r image.Rectangle, w, h int) *image.RGBA { +// if w < 0 || h < 0 { +// panic("Negative image size") +// } +// ww, hh := uint64(w), uint64(h) +// dx, dy := uint64(r.Dx()), uint64(r.Dy()) +// // See comment in Resize. +// n, sum := dx*dy, make([]uint64, 4*w*h) +// for y := r.Min.Y; y < r.Max.Y; y++ { +// pix := m.Pix[(y-r.Min.Y)*m.Stride:] +// for x := r.Min.X; x < r.Max.X; x++ { +// // Get the source pixel. +// p := pix[(x-r.Min.X)*4:] +// r64 := uint64(p[0]) +// g64 := uint64(p[1]) +// b64 := uint64(p[2]) +// a64 := uint64(p[3]) +// r64 = (r64 * a64) / 255 +// g64 = (g64 * a64) / 255 +// b64 = (b64 * a64) / 255 +// // Spread the source pixel over 1 or more destination rows. +// py := uint64(y) * hh +// for remy := hh; remy > 0; { +// qy := dy - (py % dy) +// if qy > remy { +// qy = remy +// } +// // Spread the source pixel over 1 or more destination columns. +// px := uint64(x) * ww +// index := 4 * ((py/dy)*ww + (px / dx)) +// for remx := ww; remx > 0; { +// qx := dx - (px % dx) +// if qx > remx { +// qx = remx +// } +// qxy := qx * qy +// sum[index+0] += r64 * qxy +// sum[index+1] += g64 * qxy +// sum[index+2] += b64 * qxy +// sum[index+3] += a64 * qxy +// index += 4 +// px += qx +// remx -= qx +// } +// py += qy +// remy -= qy +// } +// } +// } +// return average(sum, w, h, n) +// } + +// // Resample returns a resampled copy of the image slice r of m. +// // The returned image has width w and height h. +// func Resample(m image.Image, r image.Rectangle, w, h int) *image.RGBA { +// if w < 0 || h < 0 { +// panic("Negative image size") +// } +// if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { +// return image.NewRGBA(image.Rect(0, 0, w, h)) +// } +// curw, curh := r.Dx(), r.Dy() +// img := image.NewRGBA(image.Rect(0, 0, w, h)) +// for y := 0; y < h; y++ { +// for x := 0; x < w; x++ { +// // Get a source pixel. +// subx := x * curw / w +// suby := y * curh / h +// r32, g32, b32, a32 := m.At(subx, suby).RGBA() +// r := uint8(r32 >> 8) +// g := uint8(g32 >> 8) +// b := uint8(b32 >> 8) +// a := uint8(a32 >> 8) +// img.SetRGBA(x, y, color.RGBA{r, g, b, a}) +// } +// } +// return img +// }