Этот коммит содержится в:
Jack
2015-09-22 08:16:51 -07:00
родитель a31868336f f439c82d7c
Коммит 602bed85f2
110 изменённых файлов: 9065 добавлений и 645 удалений

3
.gitignore поставляемый
Просмотреть файл

@@ -6,7 +6,8 @@ dist
npm-debug.log
bundle*.js
model/version.go
model/version.go.bak
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o

Просмотреть файл

@@ -1,6 +1,10 @@
language: go
go:
- 1.4.2
- 1.5.1
env:
- TRAVIS_DB=mysql
- TRAVIS_DB=postgres
before_install:
- gem install compass
- sudo apt-get update -qq
@@ -24,6 +28,9 @@ before_script:
- mysql -e "CREATE DATABASE IF NOT EXISTS mattermost_test ;" -uroot
- mysql -e "CREATE USER 'mmuser'@'%' IDENTIFIED BY 'mostest' ;" -uroot
- mysql -e "GRANT ALL ON mattermost_test.* TO 'mmuser'@'%' ;" -uroot
- psql -c "create database mattermost_test ;" -U postgres
- psql -c "create user mmuser with password 'mostest' ;" -U postgres
- psql -c 'grant all privileges on database "mattermost_test" to mmuser ;' -U postgres
services:
- redis-server
addons:
@@ -38,6 +45,8 @@ deploy:
on:
repo: mattermost/platform
tags: true
go: 1.4.2
condition: $TRAVIS_DB = mysql
- provider: s3
access_key_id: AKIAJCO3KJYEGWJIKDIQ
@@ -52,3 +61,5 @@ deploy:
on:
repo: mattermost/platform
branch: master
go: 1.4.2
condition: $TRAVIS_DB = mysql

Просмотреть файл

@@ -13,7 +13,18 @@ The "UNDER DEVELOPMENT" section of the Mattermost changelog appears in the produ
### New Features
- [See Product Roadmap for anticipated features](https://mattermost.atlassian.net/issues/?filter=10002)
Messaging, Comments and Notifications
- (Preview) Added support for emoji codes rendering to image files
Admin Console
- (Preview) Ability to view server logs and change config settings
Integrations
- (Preview) Added API for incoming webhooks
- (Preview) Added OAuth2 as a service provider to allow for more secure connection to external apps
### Improvements
@@ -32,6 +43,10 @@ Performance
Code Quality
- Reformatted Javascript per Mattermost Style Guide
UI
- Added version, build number, build date and build hash under Account Settings -> Security (to be moved to "About" dialog later)
### Bug Fixes
@@ -42,7 +57,11 @@ Code Quality
Many thanks to our external contributors. In no particular order:
- TBA
- [Trozz](https://github.com/Trozz)
- [LAndres](https://github.com/LAndreas)
- [JessBot](https://github.com/JessBot)
- [apaatsio](https://github.com/apaatsio)
- [chengweiv5](https://github.com/chengweiv5)
## Release v0.7.0 (Beta1)

13
Godeps/Godeps.json сгенерированный
Просмотреть файл

@@ -12,6 +12,11 @@
"Comment": "null-236",
"Rev": "69e2a90ed92d03812364aeb947b7068dc42e561e"
},
{
"ImportPath": "code.google.com/p/graphics-go/graphics",
"Comment": "null-25",
"Rev": "f843bfcd8ac420072c7f6804599995b0a229070b"
},
{
"ImportPath": "code.google.com/p/log4go",
"Comment": "go.weekly.2012-02-22-1",
@@ -87,6 +92,14 @@
"ImportPath": "github.com/nfnt/resize",
"Rev": "dc93e1b98c579d90ee2fa15c1fd6dac34f6e7899"
},
{
"ImportPath": "github.com/rwcarlsen/goexif/exif",
"Rev": "709fab3d192d7c62f86043caff1e7e3fb0f42bd8"
},
{
"ImportPath": "github.com/rwcarlsen/goexif/tiff",
"Rev": "709fab3d192d7c62f86043caff1e7e3fb0f42bd8"
},
{
"ImportPath": "github.com/stretchr/objx",
"Rev": "cbeaeb16a013161a98496fad62933b1d21786672"

15
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/Makefile сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,15 @@
# Copyright 2011 The Graphics-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.
include $(GOROOT)/src/Make.inc
TARG=code.google.com/p/graphics-go/graphics
GOFILES=\
affine.go\
blur.go\
rotate.go\
scale.go\
thumbnail.go\
include $(GOROOT)/src/Make.pkg

174
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/affine.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,174 @@
// Copyright 2011 The Graphics-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 graphics
import (
"code.google.com/p/graphics-go/graphics/interp"
"errors"
"image"
"image/draw"
"math"
)
// I is the identity Affine transform matrix.
var I = Affine{
1, 0, 0,
0, 1, 0,
0, 0, 1,
}
// Affine is a 3x3 2D affine transform matrix.
// M(i,j) is Affine[i*3+j].
type Affine [9]float64
// Mul returns the multiplication of two affine transform matrices.
func (a Affine) Mul(b Affine) Affine {
return Affine{
a[0]*b[0] + a[1]*b[3] + a[2]*b[6],
a[0]*b[1] + a[1]*b[4] + a[2]*b[7],
a[0]*b[2] + a[1]*b[5] + a[2]*b[8],
a[3]*b[0] + a[4]*b[3] + a[5]*b[6],
a[3]*b[1] + a[4]*b[4] + a[5]*b[7],
a[3]*b[2] + a[4]*b[5] + a[5]*b[8],
a[6]*b[0] + a[7]*b[3] + a[8]*b[6],
a[6]*b[1] + a[7]*b[4] + a[8]*b[7],
a[6]*b[2] + a[7]*b[5] + a[8]*b[8],
}
}
func (a Affine) transformRGBA(dst *image.RGBA, src *image.RGBA, i interp.RGBA) error {
srcb := src.Bounds()
b := dst.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
sx, sy := a.pt(x, y)
if inBounds(srcb, sx, sy) {
c := i.RGBA(src, sx, sy)
off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
dst.Pix[off+0] = c.R
dst.Pix[off+1] = c.G
dst.Pix[off+2] = c.B
dst.Pix[off+3] = c.A
}
}
}
return nil
}
// Transform applies the affine transform to src and produces dst.
func (a Affine) Transform(dst draw.Image, src image.Image, i interp.Interp) error {
if dst == nil {
return errors.New("graphics: dst is nil")
}
if src == nil {
return errors.New("graphics: src is nil")
}
// RGBA fast path.
dstRGBA, dstOk := dst.(*image.RGBA)
srcRGBA, srcOk := src.(*image.RGBA)
interpRGBA, interpOk := i.(interp.RGBA)
if dstOk && srcOk && interpOk {
return a.transformRGBA(dstRGBA, srcRGBA, interpRGBA)
}
srcb := src.Bounds()
b := dst.Bounds()
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
sx, sy := a.pt(x, y)
if inBounds(srcb, sx, sy) {
dst.Set(x, y, i.Interp(src, sx, sy))
}
}
}
return nil
}
func inBounds(b image.Rectangle, x, y float64) bool {
if x < float64(b.Min.X) || x >= float64(b.Max.X) {
return false
}
if y < float64(b.Min.Y) || y >= float64(b.Max.Y) {
return false
}
return true
}
func (a Affine) pt(x0, y0 int) (x1, y1 float64) {
fx := float64(x0) + 0.5
fy := float64(y0) + 0.5
x1 = fx*a[0] + fy*a[1] + a[2]
y1 = fx*a[3] + fy*a[4] + a[5]
return x1, y1
}
// TransformCenter applies the affine transform to src and produces dst.
// Equivalent to
// a.CenterFit(dst, src).Transform(dst, src, i).
func (a Affine) TransformCenter(dst draw.Image, src image.Image, i interp.Interp) error {
if dst == nil {
return errors.New("graphics: dst is nil")
}
if src == nil {
return errors.New("graphics: src is nil")
}
return a.CenterFit(dst.Bounds(), src.Bounds()).Transform(dst, src, i)
}
// Scale produces a scaling transform of factors x and y.
func (a Affine) Scale(x, y float64) Affine {
return a.Mul(Affine{
1 / x, 0, 0,
0, 1 / y, 0,
0, 0, 1,
})
}
// Rotate produces a clockwise rotation transform of angle, in radians.
func (a Affine) Rotate(angle float64) Affine {
s, c := math.Sincos(angle)
return a.Mul(Affine{
+c, +s, +0,
-s, +c, +0,
+0, +0, +1,
})
}
// Shear produces a shear transform by the slopes x and y.
func (a Affine) Shear(x, y float64) Affine {
d := 1 - x*y
return a.Mul(Affine{
+1 / d, -x / d, 0,
-y / d, +1 / d, 0,
0, 0, 1,
})
}
// Translate produces a translation transform with pixel distances x and y.
func (a Affine) Translate(x, y float64) Affine {
return a.Mul(Affine{
1, 0, -x,
0, 1, -y,
0, 0, +1,
})
}
// Center produces the affine transform, centered around the provided point.
func (a Affine) Center(x, y float64) Affine {
return I.Translate(-x, -y).Mul(a).Translate(x, y)
}
// CenterFit produces the affine transform, centered around the rectangles.
// It is equivalent to
// I.Translate(-<center of src>).Mul(a).Translate(<center of dst>)
func (a Affine) CenterFit(dst, src image.Rectangle) Affine {
dx := float64(dst.Min.X) + float64(dst.Dx())/2
dy := float64(dst.Min.Y) + float64(dst.Dy())/2
sx := float64(src.Min.X) + float64(src.Dx())/2
sy := float64(src.Min.Y) + float64(src.Dy())/2
return I.Translate(-sx, -sy).Mul(a).Translate(dx, dy)
}

68
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,68 @@
// Copyright 2011 The Graphics-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 graphics
import (
"code.google.com/p/graphics-go/graphics/convolve"
"errors"
"image"
"image/draw"
"math"
)
// DefaultStdDev is the default blurring parameter.
var DefaultStdDev = 0.5
// BlurOptions are the blurring parameters.
// StdDev is the standard deviation of the normal, higher is blurrier.
// Size is the size of the kernel. If zero, it is set to Ceil(6 * StdDev).
type BlurOptions struct {
StdDev float64
Size int
}
// Blur produces a blurred version of the image, using a Gaussian blur.
func Blur(dst draw.Image, src image.Image, opt *BlurOptions) error {
if dst == nil {
return errors.New("graphics: dst is nil")
}
if src == nil {
return errors.New("graphics: src is nil")
}
sd := DefaultStdDev
size := 0
if opt != nil {
sd = opt.StdDev
size = opt.Size
}
if size < 1 {
size = int(math.Ceil(sd * 6))
}
kernel := make([]float64, 2*size+1)
for i := 0; i <= size; i++ {
x := float64(i) / sd
x = math.Pow(1/math.SqrtE, x*x)
kernel[size-i] = x
kernel[size+i] = x
}
// Normalize the weights to sum to 1.0.
kSum := 0.0
for _, k := range kernel {
kSum += k
}
for i, k := range kernel {
kernel[i] = k / kSum
}
return convolve.Convolve(dst, src, &convolve.SeparableKernel{
X: kernel,
Y: kernel,
})
}

207
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/blur_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,207 @@
// Copyright 2011 The Graphics-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 graphics
import (
"code.google.com/p/graphics-go/graphics/graphicstest"
"image"
"image/color"
"testing"
_ "image/png"
)
var blurOneColorTests = []transformOneColorTest{
{
"1x1-blank", 1, 1, 1, 1,
&BlurOptions{0.83, 1},
[]uint8{0xff},
[]uint8{0xff},
},
{
"1x1-spreadblank", 1, 1, 1, 1,
&BlurOptions{0.83, 2},
[]uint8{0xff},
[]uint8{0xff},
},
{
"3x3-blank", 3, 3, 3, 3,
&BlurOptions{0.83, 2},
[]uint8{
0xff, 0xff, 0xff,
0xff, 0xff, 0xff,
0xff, 0xff, 0xff,
},
[]uint8{
0xff, 0xff, 0xff,
0xff, 0xff, 0xff,
0xff, 0xff, 0xff,
},
},
{
"3x3-dot", 3, 3, 3, 3,
&BlurOptions{0.34, 1},
[]uint8{
0x00, 0x00, 0x00,
0x00, 0xff, 0x00,
0x00, 0x00, 0x00,
},
[]uint8{
0x00, 0x03, 0x00,
0x03, 0xf2, 0x03,
0x00, 0x03, 0x00,
},
},
{
"5x5-dot", 5, 5, 5, 5,
&BlurOptions{0.34, 1},
[]uint8{
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
},
[]uint8{
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x03, 0xf2, 0x03, 0x00,
0x00, 0x00, 0x03, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
},
},
{
"5x5-dot-spread", 5, 5, 5, 5,
&BlurOptions{0.85, 1},
[]uint8{
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
},
[]uint8{
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x20, 0x10, 0x00,
0x00, 0x20, 0x40, 0x20, 0x00,
0x00, 0x10, 0x20, 0x10, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
},
},
{
"4x4-box", 4, 4, 4, 4,
&BlurOptions{0.34, 1},
[]uint8{
0x00, 0x00, 0x00, 0x00,
0x00, 0xff, 0xff, 0x00,
0x00, 0xff, 0xff, 0x00,
0x00, 0x00, 0x00, 0x00,
},
[]uint8{
0x00, 0x03, 0x03, 0x00,
0x03, 0xf8, 0xf8, 0x03,
0x03, 0xf8, 0xf8, 0x03,
0x00, 0x03, 0x03, 0x00,
},
},
{
"5x5-twodots", 5, 5, 5, 5,
&BlurOptions{0.34, 1},
[]uint8{
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x96, 0x00, 0x96, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
},
[]uint8{
0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x02, 0x00, 0x02, 0x00,
0x02, 0x8e, 0x04, 0x8e, 0x02,
0x00, 0x02, 0x00, 0x02, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00,
},
},
}
func TestBlurOneColor(t *testing.T) {
for _, oc := range blurOneColorTests {
dst := oc.newDst()
src := oc.newSrc()
opt := oc.opt.(*BlurOptions)
if err := Blur(dst, src, opt); err != nil {
t.Fatal(err)
}
if !checkTransformTest(t, &oc, dst) {
continue
}
}
}
func TestBlurEmpty(t *testing.T) {
empty := image.NewRGBA(image.Rect(0, 0, 0, 0))
if err := Blur(empty, empty, nil); err != nil {
t.Fatal(err)
}
}
func TestBlurGopher(t *testing.T) {
src, err := graphicstest.LoadImage("../testdata/gopher.png")
if err != nil {
t.Fatal(err)
}
dst := image.NewRGBA(src.Bounds())
if err = Blur(dst, src, &BlurOptions{StdDev: 1.1}); err != nil {
t.Fatal(err)
}
cmp, err := graphicstest.LoadImage("../testdata/gopher-blur.png")
if err != nil {
t.Fatal(err)
}
err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101)
if err != nil {
t.Fatal(err)
}
}
func benchBlur(b *testing.B, bounds image.Rectangle) {
b.StopTimer()
// Construct a fuzzy image.
src := image.NewRGBA(bounds)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
src.SetRGBA(x, y, color.RGBA{
uint8(5 * x % 0x100),
uint8(7 * y % 0x100),
uint8((7*x + 5*y) % 0x100),
0xff,
})
}
}
dst := image.NewRGBA(bounds)
b.StartTimer()
for i := 0; i < b.N; i++ {
Blur(dst, src, &BlurOptions{0.84, 3})
}
}
func BenchmarkBlur400x400x3(b *testing.B) {
benchBlur(b, image.Rect(0, 0, 400, 400))
}
// Exactly twice the pixel count of 400x400.
func BenchmarkBlur400x800x3(b *testing.B) {
benchBlur(b, image.Rect(0, 0, 400, 800))
}
// Exactly twice the pixel count of 400x800
func BenchmarkBlur400x1600x3(b *testing.B) {
benchBlur(b, image.Rect(0, 0, 400, 1600))
}

11
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/Makefile сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
# Copyright 2011 The Graphics-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.
include $(GOROOT)/src/Make.inc
TARG=code.google.com/p/graphics-go/graphics/convolve
GOFILES=\
convolve.go\
include $(GOROOT)/src/Make.pkg

274
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,274 @@
// Copyright 2011 The Graphics-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 convolve
import (
"errors"
"fmt"
"image"
"image/draw"
"math"
)
// clamp clamps x to the range [x0, x1].
func clamp(x, x0, x1 float64) float64 {
if x < x0 {
return x0
}
if x > x1 {
return x1
}
return x
}
// Kernel is a square matrix that defines a convolution.
type Kernel interface {
// Weights returns the square matrix of weights in row major order.
Weights() []float64
}
// SeparableKernel is a linearly separable, square convolution kernel.
// X and Y are the per-axis weights. Each slice must be the same length, and
// have an odd length. The middle element of each slice is the weight for the
// central pixel. For example, the horizontal Sobel kernel is:
// sobelX := &SeparableKernel{
// X: []float64{-1, 0, +1},
// Y: []float64{1, 2, 1},
// }
type SeparableKernel struct {
X, Y []float64
}
func (k *SeparableKernel) Weights() []float64 {
n := len(k.X)
w := make([]float64, n*n)
for y := 0; y < n; y++ {
for x := 0; x < n; x++ {
w[y*n+x] = k.X[x] * k.Y[y]
}
}
return w
}
// fullKernel is a square convolution kernel.
type fullKernel []float64
func (k fullKernel) Weights() []float64 { return k }
func kernelSize(w []float64) (size int, err error) {
size = int(math.Sqrt(float64(len(w))))
if size*size != len(w) {
return 0, errors.New("graphics: kernel is not square")
}
if size%2 != 1 {
return 0, errors.New("graphics: kernel size is not odd")
}
return size, nil
}
// NewKernel returns a square convolution kernel.
func NewKernel(w []float64) (Kernel, error) {
if _, err := kernelSize(w); err != nil {
return nil, err
}
return fullKernel(w), nil
}
func convolveRGBASep(dst *image.RGBA, src image.Image, k *SeparableKernel) error {
if len(k.X) != len(k.Y) {
return fmt.Errorf("graphics: kernel not square (x %d, y %d)", len(k.X), len(k.Y))
}
if len(k.X)%2 != 1 {
return fmt.Errorf("graphics: kernel length (%d) not odd", len(k.X))
}
radius := (len(k.X) - 1) / 2
// buf holds the result of vertically blurring src.
bounds := dst.Bounds()
width, height := bounds.Dx(), bounds.Dy()
buf := make([]float64, width*height*4)
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
var r, g, b, a float64
// k0 is the kernel weight for the center pixel. This may be greater
// than kernel[0], near the boundary of the source image, to avoid
// vignetting.
k0 := k.X[radius]
// Add the pixels from above.
for i := 1; i <= radius; i++ {
f := k.Y[radius-i]
if y-i < bounds.Min.Y {
k0 += f
} else {
or, og, ob, oa := src.At(x, y-i).RGBA()
r += float64(or>>8) * f
g += float64(og>>8) * f
b += float64(ob>>8) * f
a += float64(oa>>8) * f
}
}
// Add the pixels from below.
for i := 1; i <= radius; i++ {
f := k.Y[radius+i]
if y+i >= bounds.Max.Y {
k0 += f
} else {
or, og, ob, oa := src.At(x, y+i).RGBA()
r += float64(or>>8) * f
g += float64(og>>8) * f
b += float64(ob>>8) * f
a += float64(oa>>8) * f
}
}
// Add the central pixel.
or, og, ob, oa := src.At(x, y).RGBA()
r += float64(or>>8) * k0
g += float64(og>>8) * k0
b += float64(ob>>8) * k0
a += float64(oa>>8) * k0
// Write to buf.
o := (y-bounds.Min.Y)*width*4 + (x-bounds.Min.X)*4
buf[o+0] = r
buf[o+1] = g
buf[o+2] = b
buf[o+3] = a
}
}
// dst holds the result of horizontally blurring buf.
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
var r, g, b, a float64
k0, off := k.X[radius], y*width*4+x*4
// Add the pixels from the left.
for i := 1; i <= radius; i++ {
f := k.X[radius-i]
if x-i < 0 {
k0 += f
} else {
o := off - i*4
r += buf[o+0] * f
g += buf[o+1] * f
b += buf[o+2] * f
a += buf[o+3] * f
}
}
// Add the pixels from the right.
for i := 1; i <= radius; i++ {
f := k.X[radius+i]
if x+i >= width {
k0 += f
} else {
o := off + i*4
r += buf[o+0] * f
g += buf[o+1] * f
b += buf[o+2] * f
a += buf[o+3] * f
}
}
// Add the central pixel.
r += buf[off+0] * k0
g += buf[off+1] * k0
b += buf[off+2] * k0
a += buf[off+3] * k0
// Write to dst, clamping to the range [0, 255].
dstOff := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
dst.Pix[dstOff+0] = uint8(clamp(r+0.5, 0, 255))
dst.Pix[dstOff+1] = uint8(clamp(g+0.5, 0, 255))
dst.Pix[dstOff+2] = uint8(clamp(b+0.5, 0, 255))
dst.Pix[dstOff+3] = uint8(clamp(a+0.5, 0, 255))
}
}
return nil
}
func convolveRGBA(dst *image.RGBA, src image.Image, k Kernel) error {
b := dst.Bounds()
bs := src.Bounds()
w := k.Weights()
size, err := kernelSize(w)
if err != nil {
return err
}
radius := (size - 1) / 2
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
if !image.Pt(x, y).In(bs) {
continue
}
var r, g, b, a, adj float64
for cy := y - radius; cy <= y+radius; cy++ {
for cx := x - radius; cx <= x+radius; cx++ {
factor := w[(cy-y+radius)*size+cx-x+radius]
if !image.Pt(cx, cy).In(bs) {
adj += factor
} else {
sr, sg, sb, sa := src.At(cx, cy).RGBA()
r += float64(sr>>8) * factor
g += float64(sg>>8) * factor
b += float64(sb>>8) * factor
a += float64(sa>>8) * factor
}
}
}
if adj != 0 {
sr, sg, sb, sa := src.At(x, y).RGBA()
r += float64(sr>>8) * adj
g += float64(sg>>8) * adj
b += float64(sb>>8) * adj
a += float64(sa>>8) * adj
}
off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
dst.Pix[off+0] = uint8(clamp(r+0.5, 0, 0xff))
dst.Pix[off+1] = uint8(clamp(g+0.5, 0, 0xff))
dst.Pix[off+2] = uint8(clamp(b+0.5, 0, 0xff))
dst.Pix[off+3] = uint8(clamp(a+0.5, 0, 0xff))
}
}
return nil
}
// Convolve produces dst by applying the convolution kernel k to src.
func Convolve(dst draw.Image, src image.Image, k Kernel) (err error) {
if dst == nil || src == nil || k == nil {
return nil
}
b := dst.Bounds()
dstRgba, ok := dst.(*image.RGBA)
if !ok {
dstRgba = image.NewRGBA(b)
}
switch k := k.(type) {
case *SeparableKernel:
err = convolveRGBASep(dstRgba, src, k)
default:
err = convolveRGBA(dstRgba, src, k)
}
if err != nil {
return err
}
if !ok {
draw.Draw(dst, b, dstRgba, b.Min, draw.Src)
}
return nil
}

78
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/convolve/convolve_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,78 @@
// Copyright 2011 The Graphics-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 convolve
import (
"code.google.com/p/graphics-go/graphics/graphicstest"
"image"
"reflect"
"testing"
_ "image/png"
)
func TestSeparableWeights(t *testing.T) {
sobelXFull := []float64{
-1, 0, 1,
-2, 0, 2,
-1, 0, 1,
}
sobelXSep := &SeparableKernel{
X: []float64{-1, 0, +1},
Y: []float64{1, 2, 1},
}
w := sobelXSep.Weights()
if !reflect.DeepEqual(w, sobelXFull) {
t.Errorf("got %v want %v", w, sobelXFull)
}
}
func TestConvolve(t *testing.T) {
kernFull, err := NewKernel([]float64{
0, 0, 0,
1, 1, 1,
0, 0, 0,
})
if err != nil {
t.Fatal(err)
}
kernSep := &SeparableKernel{
X: []float64{1, 1, 1},
Y: []float64{0, 1, 0},
}
src, err := graphicstest.LoadImage("../../testdata/gopher.png")
if err != nil {
t.Fatal(err)
}
b := src.Bounds()
sep := image.NewRGBA(b)
if err = Convolve(sep, src, kernSep); err != nil {
t.Fatal(err)
}
full := image.NewRGBA(b)
Convolve(full, src, kernFull)
err = graphicstest.ImageWithinTolerance(sep, full, 0x101)
if err != nil {
t.Fatal(err)
}
}
func TestConvolveNil(t *testing.T) {
if err := Convolve(nil, nil, nil); err != nil {
t.Fatal(err)
}
}
func TestConvolveEmpty(t *testing.T) {
empty := image.NewRGBA(image.Rect(0, 0, 0, 0))
if err := Convolve(empty, empty, nil); err != nil {
t.Fatal(err)
}
}

15
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/Makefile сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,15 @@
# Copyright 2011 The Graphics-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.
include $(GOROOT)/src/Make.inc
TARG=code.google.com/p/graphics-go/graphics
GOFILES=\
detect.go\
doc.go\
integral.go\
opencv_parser.go\
projector.go\
include $(GOROOT)/src/Make.pkg

133
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,133 @@
// Copyright 2011 The Graphics-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 detect
import (
"image"
"math"
)
// Feature is a Haar-like feature.
type Feature struct {
Rect image.Rectangle
Weight float64
}
// Classifier is a set of features with a threshold.
type Classifier struct {
Feature []Feature
Threshold float64
Left float64
Right float64
}
// CascadeStage is a cascade of classifiers.
type CascadeStage struct {
Classifier []Classifier
Threshold float64
}
// Cascade is a degenerate tree of Haar-like classifiers.
type Cascade struct {
Stage []CascadeStage
Size image.Point
}
// Match returns true if the full image is classified as an object.
func (c *Cascade) Match(m image.Image) bool {
return c.classify(newWindow(m))
}
// Find returns a set of areas of m that match the feature cascade c.
func (c *Cascade) Find(m image.Image) []image.Rectangle {
// TODO(crawshaw): Consider de-duping strategies.
matches := []image.Rectangle{}
w := newWindow(m)
b := m.Bounds()
origScale := c.Size
for s := origScale; s.X < b.Dx() && s.Y < b.Dy(); s = s.Add(s.Div(10)) {
// translate region and classify
tx := image.Pt(s.X/10, 0)
ty := image.Pt(0, s.Y/10)
for r := image.Rect(0, 0, s.X, s.Y).Add(b.Min); r.In(b); r = r.Add(ty) {
for r1 := r; r1.In(b); r1 = r1.Add(tx) {
if c.classify(w.subWindow(r1)) {
matches = append(matches, r1)
}
}
}
}
return matches
}
type window struct {
mi *integral
miSq *integral
rect image.Rectangle
invArea float64
stdDev float64
}
func (w *window) init() {
w.invArea = 1 / float64(w.rect.Dx()*w.rect.Dy())
mean := float64(w.mi.sum(w.rect)) * w.invArea
vr := float64(w.miSq.sum(w.rect))*w.invArea - mean*mean
if vr < 0 {
vr = 1
}
w.stdDev = math.Sqrt(vr)
}
func newWindow(m image.Image) *window {
mi, miSq := newIntegrals(m)
res := &window{
mi: mi,
miSq: miSq,
rect: m.Bounds(),
}
res.init()
return res
}
func (w *window) subWindow(r image.Rectangle) *window {
res := &window{
mi: w.mi,
miSq: w.miSq,
rect: r,
}
res.init()
return res
}
func (c *Classifier) classify(w *window, pr *projector) float64 {
s := 0.0
for _, f := range c.Feature {
s += float64(w.mi.sum(pr.rect(f.Rect))) * f.Weight
}
s *= w.invArea // normalize to maintain scale invariance
if s < c.Threshold*w.stdDev {
return c.Left
}
return c.Right
}
func (s *CascadeStage) classify(w *window, pr *projector) bool {
sum := 0.0
for _, c := range s.Classifier {
sum += c.classify(w, pr)
}
return sum >= s.Threshold
}
func (c *Cascade) classify(w *window) bool {
pr := newProjector(w.rect, image.Rectangle{image.Pt(0, 0), c.Size})
for _, s := range c.Stage {
if !s.classify(w, pr) {
return false
}
}
return true
}

77
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/detect_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,77 @@
// Copyright 2011 The Graphics-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 detect
import (
"image"
"image/draw"
"testing"
)
var (
c0 = Classifier{
Feature: []Feature{
Feature{Rect: image.Rect(0, 0, 3, 4), Weight: 4.0},
},
Threshold: 0.2,
Left: 0.8,
Right: 0.2,
}
c1 = Classifier{
Feature: []Feature{
Feature{Rect: image.Rect(3, 4, 4, 5), Weight: 4.0},
},
Threshold: 0.2,
Left: 0.8,
Right: 0.2,
}
c2 = Classifier{
Feature: []Feature{
Feature{Rect: image.Rect(0, 0, 1, 1), Weight: +4.0},
Feature{Rect: image.Rect(0, 0, 2, 2), Weight: -1.0},
},
Threshold: 0.2,
Left: 0.8,
Right: 0.2,
}
)
func TestClassifier(t *testing.T) {
m := image.NewGray(image.Rect(0, 0, 20, 20))
b := m.Bounds()
draw.Draw(m, image.Rect(0, 0, 20, 20), image.White, image.ZP, draw.Src)
draw.Draw(m, image.Rect(3, 4, 4, 5), image.Black, image.ZP, draw.Src)
w := newWindow(m)
pr := newProjector(b, b)
if res := c0.classify(w, pr); res != c0.Right {
t.Errorf("c0 got %f want %f", res, c0.Right)
}
if res := c1.classify(w, pr); res != c1.Left {
t.Errorf("c1 got %f want %f", res, c1.Left)
}
if res := c2.classify(w, pr); res != c1.Left {
t.Errorf("c2 got %f want %f", res, c1.Left)
}
}
func TestClassifierScale(t *testing.T) {
m := image.NewGray(image.Rect(0, 0, 50, 50))
b := m.Bounds()
draw.Draw(m, image.Rect(0, 0, 8, 10), image.White, b.Min, draw.Src)
draw.Draw(m, image.Rect(8, 10, 10, 13), image.Black, b.Min, draw.Src)
w := newWindow(m)
pr := newProjector(b, image.Rect(0, 0, 20, 20))
if res := c0.classify(w, pr); res != c0.Right {
t.Errorf("scaled c0 got %f want %f", res, c0.Right)
}
if res := c1.classify(w, pr); res != c1.Left {
t.Errorf("scaled c1 got %f want %f", res, c1.Left)
}
if res := c2.classify(w, pr); res != c1.Left {
t.Errorf("scaled c2 got %f want %f", res, c1.Left)
}
}

31
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/doc.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,31 @@
// Copyright 2011 The Graphics-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 detect implements an object detector cascade.
The technique used is a degenerate tree of Haar-like classifiers, commonly
used for face detection. It is described in
P. Viola, M. Jones.
Rapid Object Detection using a Boosted Cascade of Simple Features, 2001
IEEE Conference on Computer Vision and Pattern Recognition
A Cascade can be constructed manually from a set of Classifiers in stages,
or can be loaded from an XML file in the OpenCV format with
classifier, _, err := detect.ParseOpenCV(r)
The classifier can be used to determine if a full image is detected as an
object using Detect
if classifier.Match(m) {
// m is an image of a face.
}
It is also possible to search an image for occurrences of an object
objs := classifier.Find(m)
*/
package detect

93
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,93 @@
// Copyright 2011 The Graphics-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 detect
import (
"image"
"image/draw"
)
// integral is an image.Image-like structure that stores the cumulative
// sum of the preceding pixels. This allows for O(1) summation of any
// rectangular region within the image.
type integral struct {
// pix holds the cumulative sum of the image's pixels. The pixel at
// (x, y) starts at pix[(y-rect.Min.Y)*stride + (x-rect.Min.X)*1].
pix []uint64
stride int
rect image.Rectangle
}
func (p *integral) at(x, y int) uint64 {
return p.pix[(y-p.rect.Min.Y)*p.stride+(x-p.rect.Min.X)]
}
func (p *integral) sum(b image.Rectangle) uint64 {
c := p.at(b.Max.X-1, b.Max.Y-1)
inY := b.Min.Y > p.rect.Min.Y
inX := b.Min.X > p.rect.Min.X
if inY && inX {
c += p.at(b.Min.X-1, b.Min.Y-1)
}
if inY {
c -= p.at(b.Max.X-1, b.Min.Y-1)
}
if inX {
c -= p.at(b.Min.X-1, b.Max.Y-1)
}
return c
}
func (m *integral) integrate() {
b := m.rect
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
c := uint64(0)
if y > b.Min.Y && x > b.Min.X {
c += m.at(x-1, y)
c += m.at(x, y-1)
c -= m.at(x-1, y-1)
} else if y > b.Min.Y {
c += m.at(b.Min.X, y-1)
} else if x > b.Min.X {
c += m.at(x-1, b.Min.Y)
}
m.pix[(y-m.rect.Min.Y)*m.stride+(x-m.rect.Min.X)] += c
}
}
}
// newIntegrals returns the integral and the squared integral.
func newIntegrals(src image.Image) (*integral, *integral) {
b := src.Bounds()
srcg, ok := src.(*image.Gray)
if !ok {
srcg = image.NewGray(b)
draw.Draw(srcg, b, src, b.Min, draw.Src)
}
m := integral{
pix: make([]uint64, b.Max.Y*b.Max.X),
stride: b.Max.X,
rect: b,
}
mSq := integral{
pix: make([]uint64, b.Max.Y*b.Max.X),
stride: b.Max.X,
rect: b,
}
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
os := (y-b.Min.Y)*srcg.Stride + x - b.Min.X
om := (y-b.Min.Y)*m.stride + x - b.Min.X
c := uint64(srcg.Pix[os])
m.pix[om] = c
mSq.pix[om] = c * c
}
}
m.integrate()
mSq.integrate()
return &m, &mSq
}

156
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/integral_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,156 @@
// Copyright 2011 The Graphics-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 detect
import (
"bytes"
"fmt"
"image"
"testing"
)
type integralTest struct {
x int
y int
src []uint8
res []uint8
}
var integralTests = []integralTest{
{
1, 1,
[]uint8{0x01},
[]uint8{0x01},
},
{
2, 2,
[]uint8{
0x01, 0x02,
0x03, 0x04,
},
[]uint8{
0x01, 0x03,
0x04, 0x0a,
},
},
{
4, 4,
[]uint8{
0x02, 0x03, 0x00, 0x01,
0x01, 0x02, 0x01, 0x05,
0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01,
},
[]uint8{
0x02, 0x05, 0x05, 0x06,
0x03, 0x08, 0x09, 0x0f,
0x04, 0x0a, 0x0c, 0x13,
0x05, 0x0c, 0x0f, 0x17,
},
},
}
func sprintBox(box []byte, width, height int) string {
buf := bytes.NewBuffer(nil)
i := 0
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
fmt.Fprintf(buf, " 0x%02x,", box[i])
i++
}
buf.WriteByte('\n')
}
return buf.String()
}
func TestIntegral(t *testing.T) {
for i, oc := range integralTests {
src := &image.Gray{
Pix: oc.src,
Stride: oc.x,
Rect: image.Rect(0, 0, oc.x, oc.y),
}
dst, _ := newIntegrals(src)
res := make([]byte, len(dst.pix))
for i, p := range dst.pix {
res[i] = byte(p)
}
if !bytes.Equal(res, oc.res) {
got := sprintBox(res, oc.x, oc.y)
want := sprintBox(oc.res, oc.x, oc.y)
t.Errorf("%d: got\n%s\n want\n%s", i, got, want)
}
}
}
func TestIntegralSum(t *testing.T) {
src := &image.Gray{
Pix: []uint8{
0x02, 0x03, 0x00, 0x01, 0x03,
0x01, 0x02, 0x01, 0x05, 0x05,
0x01, 0x01, 0x01, 0x01, 0x02,
0x01, 0x01, 0x01, 0x01, 0x07,
0x02, 0x01, 0x00, 0x03, 0x01,
},
Stride: 5,
Rect: image.Rect(0, 0, 5, 5),
}
img, _ := newIntegrals(src)
type sumTest struct {
rect image.Rectangle
sum uint64
}
var sumTests = []sumTest{
{image.Rect(0, 0, 1, 1), 2},
{image.Rect(0, 0, 2, 1), 5},
{image.Rect(0, 0, 1, 3), 4},
{image.Rect(1, 1, 3, 3), 5},
{image.Rect(2, 2, 4, 4), 4},
{image.Rect(4, 3, 5, 5), 8},
{image.Rect(2, 4, 3, 5), 0},
}
for _, st := range sumTests {
s := img.sum(st.rect)
if s != st.sum {
t.Errorf("%v: got %d want %d", st.rect, s, st.sum)
return
}
}
}
func TestIntegralSubImage(t *testing.T) {
m0 := &image.Gray{
Pix: []uint8{
0x02, 0x03, 0x00, 0x01, 0x03,
0x01, 0x02, 0x01, 0x05, 0x05,
0x01, 0x04, 0x01, 0x01, 0x02,
0x01, 0x02, 0x01, 0x01, 0x07,
0x02, 0x01, 0x09, 0x03, 0x01,
},
Stride: 5,
Rect: image.Rect(0, 0, 5, 5),
}
b := image.Rect(1, 1, 4, 4)
m1 := m0.SubImage(b)
mi0, _ := newIntegrals(m0)
mi1, _ := newIntegrals(m1)
sum0 := mi0.sum(b)
sum1 := mi1.sum(b)
if sum0 != sum1 {
t.Errorf("b got %d want %d", sum0, sum1)
}
r0 := image.Rect(2, 2, 4, 4)
sum0 = mi0.sum(r0)
sum1 = mi1.sum(r0)
if sum0 != sum1 {
t.Errorf("r0 got %d want %d", sum1, sum0)
}
}

125
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,125 @@
// Copyright 2011 The Graphics-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 detect
import (
"bytes"
"encoding/xml"
"errors"
"fmt"
"image"
"io"
"io/ioutil"
"strconv"
"strings"
)
type xmlFeature struct {
Rects []string `xml:"grp>feature>rects>grp"`
Tilted int `xml:"grp>feature>tilted"`
Threshold float64 `xml:"grp>threshold"`
Left float64 `xml:"grp>left_val"`
Right float64 `xml:"grp>right_val"`
}
type xmlStages struct {
Trees []xmlFeature `xml:"trees>grp"`
Stage_threshold float64 `xml:"stage_threshold"`
Parent int `xml:"parent"`
Next int `xml:"next"`
}
type opencv_storage struct {
Any struct {
XMLName xml.Name
Type string `xml:"type_id,attr"`
Size string `xml:"size"`
Stages []xmlStages `xml:"stages>grp"`
} `xml:",any"`
}
func buildFeature(r string) (f Feature, err error) {
var x, y, w, h int
var weight float64
_, err = fmt.Sscanf(r, "%d %d %d %d %f", &x, &y, &w, &h, &weight)
if err != nil {
return
}
f.Rect = image.Rect(x, y, x+w, y+h)
f.Weight = weight
return
}
func buildCascade(s *opencv_storage) (c *Cascade, name string, err error) {
if s.Any.Type != "opencv-haar-classifier" {
err = fmt.Errorf("got %s want opencv-haar-classifier", s.Any.Type)
return
}
name = s.Any.XMLName.Local
c = &Cascade{}
sizes := strings.Split(s.Any.Size, " ")
w, err := strconv.Atoi(sizes[0])
if err != nil {
return nil, "", err
}
h, err := strconv.Atoi(sizes[1])
if err != nil {
return nil, "", err
}
c.Size = image.Pt(w, h)
c.Stage = []CascadeStage{}
for _, stage := range s.Any.Stages {
cs := CascadeStage{
Classifier: []Classifier{},
Threshold: stage.Stage_threshold,
}
for _, tree := range stage.Trees {
if tree.Tilted != 0 {
err = errors.New("Cascade does not support tilted features")
return
}
cls := Classifier{
Feature: []Feature{},
Threshold: tree.Threshold,
Left: tree.Left,
Right: tree.Right,
}
for _, rect := range tree.Rects {
f, err := buildFeature(rect)
if err != nil {
return nil, "", err
}
cls.Feature = append(cls.Feature, f)
}
cs.Classifier = append(cs.Classifier, cls)
}
c.Stage = append(c.Stage, cs)
}
return
}
// ParseOpenCV produces a detection Cascade from an OpenCV XML file.
func ParseOpenCV(r io.Reader) (cascade *Cascade, name string, err error) {
// BUG(crawshaw): tag-based parsing doesn't seem to work with <_>
buf, err := ioutil.ReadAll(r)
if err != nil {
return
}
buf = bytes.Replace(buf, []byte("<_>"), []byte("<grp>"), -1)
buf = bytes.Replace(buf, []byte("</_>"), []byte("</grp>"), -1)
s := &opencv_storage{}
err = xml.Unmarshal(buf, s)
if err != nil {
return
}
return buildCascade(s)
}

75
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/opencv_parser_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,75 @@
// Copyright 2011 The Graphics-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 detect
import (
"image"
"os"
"reflect"
"testing"
)
var (
classifier0 = Classifier{
Feature: []Feature{
Feature{Rect: image.Rect(0, 0, 3, 4), Weight: -1},
Feature{Rect: image.Rect(3, 4, 5, 6), Weight: 3.1},
},
Threshold: 0.03,
Left: 0.01,
Right: 0.8,
}
classifier1 = Classifier{
Feature: []Feature{
Feature{Rect: image.Rect(3, 7, 17, 11), Weight: -3.2},
Feature{Rect: image.Rect(3, 9, 17, 11), Weight: 2.},
},
Threshold: 0.11,
Left: 0.03,
Right: 0.83,
}
classifier2 = Classifier{
Feature: []Feature{
Feature{Rect: image.Rect(1, 1, 3, 3), Weight: -1.},
Feature{Rect: image.Rect(3, 3, 5, 5), Weight: 2.5},
},
Threshold: 0.07,
Left: 0.2,
Right: 0.4,
}
cascade = Cascade{
Stage: []CascadeStage{
CascadeStage{
Classifier: []Classifier{classifier0, classifier1},
Threshold: 0.82,
},
CascadeStage{
Classifier: []Classifier{classifier2},
Threshold: 0.22,
},
},
Size: image.Pt(20, 20),
}
)
func TestParseOpenCV(t *testing.T) {
file, err := os.Open("../../testdata/opencv.xml")
if err != nil {
t.Fatal(err)
}
defer file.Close()
cascadeFile, name, err := ParseOpenCV(file)
if err != nil {
t.Fatal(err)
}
if name != "name_of_cascade" {
t.Fatalf("name: got %s want name_of_cascade", name)
}
if !reflect.DeepEqual(cascade, *cascadeFile) {
t.Errorf("got\n %v want\n %v", *cascadeFile, cascade)
}
}

55
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,55 @@
// Copyright 2011 The Graphics-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 detect
import (
"image"
)
// projector allows projecting from a source Rectangle onto a target Rectangle.
type projector struct {
// rx, ry is the scaling factor.
rx, ry float64
// dx, dy is the translation factor.
dx, dy float64
// r is the clipping region of the target.
r image.Rectangle
}
// newProjector creates a Projector with source src and target dst.
func newProjector(dst image.Rectangle, src image.Rectangle) *projector {
return &projector{
rx: float64(dst.Dx()) / float64(src.Dx()),
ry: float64(dst.Dy()) / float64(src.Dy()),
dx: float64(dst.Min.X - src.Min.X),
dy: float64(dst.Min.Y - src.Min.Y),
r: dst,
}
}
// pt projects p from the source rectangle onto the target rectangle.
func (s *projector) pt(p image.Point) image.Point {
return image.Point{
clamp(s.rx*float64(p.X)+s.dx, s.r.Min.X, s.r.Max.X),
clamp(s.ry*float64(p.Y)+s.dy, s.r.Min.Y, s.r.Max.Y),
}
}
// rect projects r from the source rectangle onto the target rectangle.
func (s *projector) rect(r image.Rectangle) image.Rectangle {
return image.Rectangle{s.pt(r.Min), s.pt(r.Max)}
}
// clamp rounds and clamps o to the integer range [x0, x1].
func clamp(o float64, x0, x1 int) int {
x := int(o + 0.5)
if x < x0 {
return x0
}
if x > x1 {
return x1
}
return x
}

49
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/detect/projector_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,49 @@
// Copyright 2011 The Graphics-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 detect
import (
"image"
"reflect"
"testing"
)
type projectorTest struct {
dst image.Rectangle
src image.Rectangle
pdst image.Rectangle
psrc image.Rectangle
}
var projectorTests = []projectorTest{
{
image.Rect(0, 0, 6, 6),
image.Rect(0, 0, 2, 2),
image.Rect(0, 0, 6, 6),
image.Rect(0, 0, 2, 2),
},
{
image.Rect(0, 0, 6, 6),
image.Rect(0, 0, 2, 2),
image.Rect(3, 3, 6, 6),
image.Rect(1, 1, 2, 2),
},
{
image.Rect(30, 30, 40, 40),
image.Rect(10, 10, 20, 20),
image.Rect(32, 33, 34, 37),
image.Rect(12, 13, 14, 17),
},
}
func TestProjector(t *testing.T) {
for i, tt := range projectorTests {
pr := newProjector(tt.dst, tt.src)
res := pr.rect(tt.psrc)
if !reflect.DeepEqual(res, tt.pdst) {
t.Errorf("%d: got %v want %v", i, res, tt.pdst)
}
}
}

11
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/Makefile сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
# Copyright 2011 The Graphics-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.
include $(GOROOT)/src/Make.inc
TARG=code.google.com/p/graphics-go/graphics/graphicstest
GOFILES=\
graphicstest.go\
include $(GOROOT)/src/Make.pkg

112
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/graphicstest/graphicstest.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,112 @@
// Copyright 2011 The Graphics-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 graphicstest
import (
"bytes"
"errors"
"fmt"
"image"
"image/color"
"os"
)
// LoadImage decodes an image from a file.
func LoadImage(path string) (img image.Image, err error) {
file, err := os.Open(path)
if err != nil {
return
}
defer file.Close()
img, _, err = image.Decode(file)
return
}
func delta(u0, u1 uint32) int {
d := int(u0) - int(u1)
if d < 0 {
return -d
}
return d
}
func withinTolerance(c0, c1 color.Color, tol int) bool {
r0, g0, b0, a0 := c0.RGBA()
r1, g1, b1, a1 := c1.RGBA()
r := delta(r0, r1)
g := delta(g0, g1)
b := delta(b0, b1)
a := delta(a0, a1)
return r <= tol && g <= tol && b <= tol && a <= tol
}
// ImageWithinTolerance checks that each pixel varies by no more than tol.
func ImageWithinTolerance(m0, m1 image.Image, tol int) error {
b0 := m0.Bounds()
b1 := m1.Bounds()
if !b0.Eq(b1) {
return errors.New(fmt.Sprintf("got bounds %v want %v", b0, b1))
}
for y := b0.Min.Y; y < b0.Max.Y; y++ {
for x := b0.Min.X; x < b0.Max.X; x++ {
c0 := m0.At(x, y)
c1 := m1.At(x, y)
if !withinTolerance(c0, c1, tol) {
e := fmt.Sprintf("got %v want %v at (%d, %d)", c0, c1, x, y)
return errors.New(e)
}
}
}
return nil
}
// SprintBox pretty prints the array as a hexidecimal matrix.
func SprintBox(box []byte, width, height int) string {
buf := bytes.NewBuffer(nil)
i := 0
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
fmt.Fprintf(buf, " 0x%02x,", box[i])
i++
}
buf.WriteByte('\n')
}
return buf.String()
}
// SprintImageR pretty prints the red channel of src. It looks like SprintBox.
func SprintImageR(src *image.RGBA) string {
w, h := src.Rect.Dx(), src.Rect.Dy()
i := 0
box := make([]byte, w*h)
for y := src.Rect.Min.Y; y < src.Rect.Max.Y; y++ {
for x := src.Rect.Min.X; x < src.Rect.Max.X; x++ {
off := (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4
box[i] = src.Pix[off]
i++
}
}
return SprintBox(box, w, h)
}
// MakeRGBA returns an image with R, G, B taken from src.
func MakeRGBA(src []uint8, width int) *image.RGBA {
b := image.Rect(0, 0, width, len(src)/width)
ret := image.NewRGBA(b)
i := 0
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
ret.SetRGBA(x, y, color.RGBA{
R: src[i],
G: src[i],
B: src[i],
A: 0xff,
})
i++
}
}
return ret
}

13
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/Makefile сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,13 @@
# Copyright 2012 The Graphics-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.
include $(GOROOT)/src/Make.inc
TARG=code.google.com/p/graphics-go/graphics/interp
GOFILES=\
bilinear.go\
doc.go\
interp.go\
include $(GOROOT)/src/Make.pkg

206
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,206 @@
// Copyright 2012 The Graphics-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 interp
import (
"image"
"image/color"
"math"
)
// Bilinear implements bilinear interpolation.
var Bilinear Interp = bilinear{}
type bilinear struct{}
func (i bilinear) Interp(src image.Image, x, y float64) color.Color {
if src, ok := src.(*image.RGBA); ok {
return i.RGBA(src, x, y)
}
return bilinearGeneral(src, x, y)
}
func bilinearGeneral(src image.Image, x, y float64) color.Color {
p := findLinearSrc(src.Bounds(), x, y)
var fr, fg, fb, fa float64
var r, g, b, a uint32
r, g, b, a = src.At(p.low.X, p.low.Y).RGBA()
fr += float64(r) * p.frac00
fg += float64(g) * p.frac00
fb += float64(b) * p.frac00
fa += float64(a) * p.frac00
r, g, b, a = src.At(p.high.X, p.low.Y).RGBA()
fr += float64(r) * p.frac01
fg += float64(g) * p.frac01
fb += float64(b) * p.frac01
fa += float64(a) * p.frac01
r, g, b, a = src.At(p.low.X, p.high.Y).RGBA()
fr += float64(r) * p.frac10
fg += float64(g) * p.frac10
fb += float64(b) * p.frac10
fa += float64(a) * p.frac10
r, g, b, a = src.At(p.high.X, p.high.Y).RGBA()
fr += float64(r) * p.frac11
fg += float64(g) * p.frac11
fb += float64(b) * p.frac11
fa += float64(a) * p.frac11
var c color.RGBA64
c.R = uint16(fr + 0.5)
c.G = uint16(fg + 0.5)
c.B = uint16(fb + 0.5)
c.A = uint16(fa + 0.5)
return c
}
func (bilinear) RGBA(src *image.RGBA, x, y float64) color.RGBA {
p := findLinearSrc(src.Bounds(), x, y)
// Array offsets for the surrounding pixels.
off00 := offRGBA(src, p.low.X, p.low.Y)
off01 := offRGBA(src, p.high.X, p.low.Y)
off10 := offRGBA(src, p.low.X, p.high.Y)
off11 := offRGBA(src, p.high.X, p.high.Y)
var fr, fg, fb, fa float64
fr += float64(src.Pix[off00+0]) * p.frac00
fg += float64(src.Pix[off00+1]) * p.frac00
fb += float64(src.Pix[off00+2]) * p.frac00
fa += float64(src.Pix[off00+3]) * p.frac00
fr += float64(src.Pix[off01+0]) * p.frac01
fg += float64(src.Pix[off01+1]) * p.frac01
fb += float64(src.Pix[off01+2]) * p.frac01
fa += float64(src.Pix[off01+3]) * p.frac01
fr += float64(src.Pix[off10+0]) * p.frac10
fg += float64(src.Pix[off10+1]) * p.frac10
fb += float64(src.Pix[off10+2]) * p.frac10
fa += float64(src.Pix[off10+3]) * p.frac10
fr += float64(src.Pix[off11+0]) * p.frac11
fg += float64(src.Pix[off11+1]) * p.frac11
fb += float64(src.Pix[off11+2]) * p.frac11
fa += float64(src.Pix[off11+3]) * p.frac11
var c color.RGBA
c.R = uint8(fr + 0.5)
c.G = uint8(fg + 0.5)
c.B = uint8(fb + 0.5)
c.A = uint8(fa + 0.5)
return c
}
func (bilinear) Gray(src *image.Gray, x, y float64) color.Gray {
p := findLinearSrc(src.Bounds(), x, y)
// Array offsets for the surrounding pixels.
off00 := offGray(src, p.low.X, p.low.Y)
off01 := offGray(src, p.high.X, p.low.Y)
off10 := offGray(src, p.low.X, p.high.Y)
off11 := offGray(src, p.high.X, p.high.Y)
var fc float64
fc += float64(src.Pix[off00]) * p.frac00
fc += float64(src.Pix[off01]) * p.frac01
fc += float64(src.Pix[off10]) * p.frac10
fc += float64(src.Pix[off11]) * p.frac11
var c color.Gray
c.Y = uint8(fc + 0.5)
return c
}
type bilinearSrc struct {
// Top-left and bottom-right interpolation sources
low, high image.Point
// Fraction of each pixel to take. The 0 suffix indicates
// top/left, and the 1 suffix indicates bottom/right.
frac00, frac01, frac10, frac11 float64
}
func findLinearSrc(b image.Rectangle, sx, sy float64) bilinearSrc {
maxX := float64(b.Max.X)
maxY := float64(b.Max.Y)
minX := float64(b.Min.X)
minY := float64(b.Min.Y)
lowX := math.Floor(sx - 0.5)
lowY := math.Floor(sy - 0.5)
if lowX < minX {
lowX = minX
}
if lowY < minY {
lowY = minY
}
highX := math.Ceil(sx - 0.5)
highY := math.Ceil(sy - 0.5)
if highX >= maxX {
highX = maxX - 1
}
if highY >= maxY {
highY = maxY - 1
}
// In the variables below, the 0 suffix indicates top/left, and the
// 1 suffix indicates bottom/right.
// Center of each surrounding pixel.
x00 := lowX + 0.5
y00 := lowY + 0.5
x01 := highX + 0.5
y01 := lowY + 0.5
x10 := lowX + 0.5
y10 := highY + 0.5
x11 := highX + 0.5
y11 := highY + 0.5
p := bilinearSrc{
low: image.Pt(int(lowX), int(lowY)),
high: image.Pt(int(highX), int(highY)),
}
// Literally, edge cases. If we are close enough to the edge of
// the image, curtail the interpolation sources.
if lowX == highX && lowY == highY {
p.frac00 = 1.0
} else if sy-minY <= 0.5 && sx-minX <= 0.5 {
p.frac00 = 1.0
} else if maxY-sy <= 0.5 && maxX-sx <= 0.5 {
p.frac11 = 1.0
} else if sy-minY <= 0.5 || lowY == highY {
p.frac00 = x01 - sx
p.frac01 = sx - x00
} else if sx-minX <= 0.5 || lowX == highX {
p.frac00 = y10 - sy
p.frac10 = sy - y00
} else if maxY-sy <= 0.5 {
p.frac10 = x11 - sx
p.frac11 = sx - x10
} else if maxX-sx <= 0.5 {
p.frac01 = y11 - sy
p.frac11 = sy - y01
} else {
p.frac00 = (x01 - sx) * (y10 - sy)
p.frac01 = (sx - x00) * (y11 - sy)
p.frac10 = (x11 - sx) * (sy - y00)
p.frac11 = (sx - x10) * (sy - y01)
}
return p
}
// TODO(crawshaw): When we have inlining, consider func (p *RGBA) Off(x, y) int
func offRGBA(src *image.RGBA, x, y int) int {
return (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4
}
func offGray(src *image.Gray, x, y int) int {
return (y-src.Rect.Min.Y)*src.Stride + (x - src.Rect.Min.X)
}

143
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/bilinear_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,143 @@
// Copyright 2012 The Graphics-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 interp
import (
"image"
"image/color"
"testing"
)
type interpTest struct {
desc string
src []uint8
srcWidth int
x, y float64
expect uint8
}
func (p *interpTest) newSrc() *image.RGBA {
b := image.Rect(0, 0, p.srcWidth, len(p.src)/p.srcWidth)
src := image.NewRGBA(b)
i := 0
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
src.SetRGBA(x, y, color.RGBA{
R: p.src[i],
G: p.src[i],
B: p.src[i],
A: 0xff,
})
i++
}
}
return src
}
var interpTests = []interpTest{
{
desc: "center of a single white pixel should match that pixel",
src: []uint8{0x00},
srcWidth: 1,
x: 0.5,
y: 0.5,
expect: 0x00,
},
{
desc: "middle of a square is equally weighted",
src: []uint8{
0x00, 0xff,
0xff, 0x00,
},
srcWidth: 2,
x: 1.0,
y: 1.0,
expect: 0x80,
},
{
desc: "center of a pixel is just that pixel",
src: []uint8{
0x00, 0xff,
0xff, 0x00,
},
srcWidth: 2,
x: 1.5,
y: 0.5,
expect: 0xff,
},
{
desc: "asymmetry abounds",
src: []uint8{
0xaa, 0x11, 0x55,
0xff, 0x95, 0xdd,
},
srcWidth: 3,
x: 2.0,
y: 1.0,
expect: 0x76, // (0x11 + 0x55 + 0x95 + 0xdd) / 4
},
}
func TestBilinearRGBA(t *testing.T) {
for _, p := range interpTests {
src := p.newSrc()
// Fast path.
c := Bilinear.(RGBA).RGBA(src, p.x, p.y)
if c.R != c.G || c.R != c.B || c.A != 0xff {
t.Errorf("expect channels to match, got %v", c)
continue
}
if c.R != p.expect {
t.Errorf("%s: got 0x%02x want 0x%02x", p.desc, c.R, p.expect)
continue
}
// Standard Interp should use the fast path.
cStd := Bilinear.Interp(src, p.x, p.y)
if cStd != c {
t.Errorf("%s: standard mismatch got %v want %v", p.desc, cStd, c)
continue
}
// General case should match the fast path.
cGen := color.RGBAModel.Convert(bilinearGeneral(src, p.x, p.y))
r0, g0, b0, a0 := c.RGBA()
r1, g1, b1, a1 := cGen.RGBA()
if r0 != r1 || g0 != g1 || b0 != b1 || a0 != a1 {
t.Errorf("%s: general case mismatch got %v want %v", p.desc, c, cGen)
continue
}
}
}
func TestBilinearSubImage(t *testing.T) {
b0 := image.Rect(0, 0, 4, 4)
src0 := image.NewRGBA(b0)
b1 := image.Rect(1, 1, 3, 3)
src1 := src0.SubImage(b1).(*image.RGBA)
src1.Set(1, 1, color.RGBA{0x11, 0, 0, 0xff})
src1.Set(2, 1, color.RGBA{0x22, 0, 0, 0xff})
src1.Set(1, 2, color.RGBA{0x33, 0, 0, 0xff})
src1.Set(2, 2, color.RGBA{0x44, 0, 0, 0xff})
tests := []struct {
x, y float64
want uint8
}{
{1, 1, 0x11},
{3, 1, 0x22},
{1, 3, 0x33},
{3, 3, 0x44},
{2, 2, 0x2b},
}
for _, p := range tests {
c := Bilinear.(RGBA).RGBA(src1, p.x, p.y)
if c.R != p.want {
t.Errorf("(%.0f, %.0f): got 0x%02x want 0x%02x", p.x, p.y, c.R, p.want)
}
}
}

25
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/doc.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,25 @@
// Copyright 2012 The Graphics-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 interp implements image interpolation.
An interpolator provides the Interp interface, which can be used
to interpolate a pixel:
c := interp.Bilinear.Interp(src, 1.2, 1.8)
To interpolate a large number of RGBA or Gray pixels, an implementation
may provide a fast-path by implementing the RGBA or Gray interfaces.
i1, ok := i.(interp.RGBA)
if ok {
c := i1.RGBA(src, 1.2, 1.8)
// use c.R, c.G, etc
return
}
c := i.Interp(src, 1.2, 1.8)
// use generic color.Color
*/
package interp

29
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/interp/interp.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,29 @@
// Copyright 2012 The Graphics-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 interp
import (
"image"
"image/color"
)
// Interp interpolates an image's color at fractional co-ordinates.
type Interp interface {
// Interp interpolates (x, y).
Interp(src image.Image, x, y float64) color.Color
}
// RGBA is a fast-path interpolation implementation for image.RGBA.
// It is common for an Interp to also implement RGBA.
type RGBA interface {
// RGBA interpolates (x, y).
RGBA(src *image.RGBA, x, y float64) color.RGBA
}
// Gray is a fast-path interpolation implementation for image.Gray.
type Gray interface {
// Gray interpolates (x, y).
Gray(src *image.Gray, x, y float64) color.Gray
}

35
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,35 @@
// Copyright 2011 The Graphics-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 graphics
import (
"code.google.com/p/graphics-go/graphics/interp"
"errors"
"image"
"image/draw"
)
// RotateOptions are the rotation parameters.
// Angle is the angle, in radians, to rotate the image clockwise.
type RotateOptions struct {
Angle float64
}
// Rotate produces a rotated version of src, drawn onto dst.
func Rotate(dst draw.Image, src image.Image, opt *RotateOptions) error {
if dst == nil {
return errors.New("graphics: dst is nil")
}
if src == nil {
return errors.New("graphics: src is nil")
}
angle := 0.0
if opt != nil {
angle = opt.Angle
}
return I.Rotate(angle).TransformCenter(dst, src, interp.Bilinear)
}

169
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/rotate_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,169 @@
// Copyright 2011 The Graphics-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 graphics
import (
"code.google.com/p/graphics-go/graphics/graphicstest"
"image"
"math"
"testing"
_ "image/png"
)
var rotateOneColorTests = []transformOneColorTest{
{
"onepixel-onequarter", 1, 1, 1, 1,
&RotateOptions{math.Pi / 2},
[]uint8{0xff},
[]uint8{0xff},
},
{
"onepixel-partial", 1, 1, 1, 1,
&RotateOptions{math.Pi * 2.0 / 3.0},
[]uint8{0xff},
[]uint8{0xff},
},
{
"onepixel-complete", 1, 1, 1, 1,
&RotateOptions{2 * math.Pi},
[]uint8{0xff},
[]uint8{0xff},
},
{
"even-onequarter", 2, 2, 2, 2,
&RotateOptions{math.Pi / 2.0},
[]uint8{
0xff, 0x00,
0x00, 0xff,
},
[]uint8{
0x00, 0xff,
0xff, 0x00,
},
},
{
"even-complete", 2, 2, 2, 2,
&RotateOptions{2.0 * math.Pi},
[]uint8{
0xff, 0x00,
0x00, 0xff,
},
[]uint8{
0xff, 0x00,
0x00, 0xff,
},
},
{
"line-partial", 3, 3, 3, 3,
&RotateOptions{math.Pi * 1.0 / 3.0},
[]uint8{
0x00, 0x00, 0x00,
0xff, 0xff, 0xff,
0x00, 0x00, 0x00,
},
[]uint8{
0xa2, 0x80, 0x00,
0x22, 0xff, 0x22,
0x00, 0x80, 0xa2,
},
},
{
"line-offset-partial", 3, 3, 3, 3,
&RotateOptions{math.Pi * 3 / 2},
[]uint8{
0x00, 0x00, 0x00,
0x00, 0xff, 0xff,
0x00, 0x00, 0x00,
},
[]uint8{
0x00, 0xff, 0x00,
0x00, 0xff, 0x00,
0x00, 0x00, 0x00,
},
},
{
"dot-partial", 4, 4, 4, 4,
&RotateOptions{math.Pi},
[]uint8{
0x00, 0x00, 0x00, 0x00,
0x00, 0xff, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
},
[]uint8{
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xff, 0x00,
0x00, 0x00, 0x00, 0x00,
},
},
}
func TestRotateOneColor(t *testing.T) {
for _, oc := range rotateOneColorTests {
src := oc.newSrc()
dst := oc.newDst()
if err := Rotate(dst, src, oc.opt.(*RotateOptions)); err != nil {
t.Errorf("rotate %s: %v", oc.desc, err)
continue
}
if !checkTransformTest(t, &oc, dst) {
continue
}
}
}
func TestRotateEmpty(t *testing.T) {
empty := image.NewRGBA(image.Rect(0, 0, 0, 0))
if err := Rotate(empty, empty, nil); err != nil {
t.Fatal(err)
}
}
func TestRotateGopherSide(t *testing.T) {
src, err := graphicstest.LoadImage("../testdata/gopher.png")
if err != nil {
t.Fatal(err)
}
srcb := src.Bounds()
dst := image.NewRGBA(image.Rect(0, 0, srcb.Dy(), srcb.Dx()))
if err := Rotate(dst, src, &RotateOptions{math.Pi / 2.0}); err != nil {
t.Fatal(err)
}
cmp, err := graphicstest.LoadImage("../testdata/gopher-rotate-side.png")
if err != nil {
t.Fatal(err)
}
err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101)
if err != nil {
t.Fatal(err)
}
}
func TestRotateGopherPartial(t *testing.T) {
src, err := graphicstest.LoadImage("../testdata/gopher.png")
if err != nil {
t.Fatal(err)
}
srcb := src.Bounds()
dst := image.NewRGBA(image.Rect(0, 0, srcb.Dx(), srcb.Dy()))
if err := Rotate(dst, src, &RotateOptions{math.Pi / 3.0}); err != nil {
t.Fatal(err)
}
cmp, err := graphicstest.LoadImage("../testdata/gopher-rotate-partial.png")
if err != nil {
t.Fatal(err)
}
err = graphicstest.ImageWithinTolerance(dst, cmp, 0x101)
if err != nil {
t.Fatal(err)
}
}

31
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,31 @@
// Copyright 2011 The Graphics-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 graphics
import (
"code.google.com/p/graphics-go/graphics/interp"
"errors"
"image"
"image/draw"
)
// Scale produces a scaled version of the image using bilinear interpolation.
func Scale(dst draw.Image, src image.Image) error {
if dst == nil {
return errors.New("graphics: dst is nil")
}
if src == nil {
return errors.New("graphics: src is nil")
}
b := dst.Bounds()
srcb := src.Bounds()
if b.Empty() || srcb.Empty() {
return nil
}
sx := float64(b.Dx()) / float64(srcb.Dx())
sy := float64(b.Dy()) / float64(srcb.Dy())
return I.Scale(sx, sy).Transform(dst, src, interp.Bilinear)
}

153
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/scale_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,153 @@
// Copyright 2011 The Graphics-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 graphics
import (
"code.google.com/p/graphics-go/graphics/graphicstest"
"image"
"testing"
_ "image/png"
)
var scaleOneColorTests = []transformOneColorTest{
{
"down-half",
1, 1,
2, 2,
nil,
[]uint8{
0x80, 0x00,
0x00, 0x80,
},
[]uint8{
0x40,
},
},
{
"up-double",
4, 4,
2, 2,
nil,
[]uint8{
0x80, 0x00,
0x00, 0x80,
},
[]uint8{
0x80, 0x60, 0x20, 0x00,
0x60, 0x50, 0x30, 0x20,
0x20, 0x30, 0x50, 0x60,
0x00, 0x20, 0x60, 0x80,
},
},
{
"up-doublewidth",
4, 2,
2, 2,
nil,
[]uint8{
0x80, 0x00,
0x00, 0x80,
},
[]uint8{
0x80, 0x60, 0x20, 0x00,
0x00, 0x20, 0x60, 0x80,
},
},
{
"up-doubleheight",
2, 4,
2, 2,
nil,
[]uint8{
0x80, 0x00,
0x00, 0x80,
},
[]uint8{
0x80, 0x00,
0x60, 0x20,
0x20, 0x60,
0x00, 0x80,
},
},
{
"up-partial",
3, 3,
2, 2,
nil,
[]uint8{
0x80, 0x00,
0x00, 0x80,
},
[]uint8{
0x80, 0x40, 0x00,
0x40, 0x40, 0x40,
0x00, 0x40, 0x80,
},
},
}
func TestScaleOneColor(t *testing.T) {
for _, oc := range scaleOneColorTests {
dst := oc.newDst()
src := oc.newSrc()
if err := Scale(dst, src); err != nil {
t.Errorf("scale %s: %v", oc.desc, err)
continue
}
if !checkTransformTest(t, &oc, dst) {
continue
}
}
}
func TestScaleEmpty(t *testing.T) {
empty := image.NewRGBA(image.Rect(0, 0, 0, 0))
if err := Scale(empty, empty); err != nil {
t.Fatal(err)
}
}
func TestScaleGopher(t *testing.T) {
dst := image.NewRGBA(image.Rect(0, 0, 100, 150))
src, err := graphicstest.LoadImage("../testdata/gopher.png")
if err != nil {
t.Error(err)
return
}
// Down-sample.
if err := Scale(dst, src); err != nil {
t.Fatal(err)
}
cmp, err := graphicstest.LoadImage("../testdata/gopher-100x150.png")
if err != nil {
t.Error(err)
return
}
err = graphicstest.ImageWithinTolerance(dst, cmp, 0)
if err != nil {
t.Error(err)
return
}
// Up-sample.
dst = image.NewRGBA(image.Rect(0, 0, 500, 750))
if err := Scale(dst, src); err != nil {
t.Fatal(err)
}
cmp, err = graphicstest.LoadImage("../testdata/gopher-500x750.png")
if err != nil {
t.Error(err)
return
}
err = graphicstest.ImageWithinTolerance(dst, cmp, 0)
if err != nil {
t.Error(err)
return
}
}

69
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/shared_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,69 @@
// Copyright 2011 The Graphics-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 graphics
import (
"bytes"
"code.google.com/p/graphics-go/graphics/graphicstest"
"image"
"image/color"
"testing"
)
type transformOneColorTest struct {
desc string
dstWidth int
dstHeight int
srcWidth int
srcHeight int
opt interface{}
src []uint8
res []uint8
}
func (oc *transformOneColorTest) newSrc() *image.RGBA {
b := image.Rect(0, 0, oc.srcWidth, oc.srcHeight)
src := image.NewRGBA(b)
i := 0
for y := b.Min.Y; y < b.Max.Y; y++ {
for x := b.Min.X; x < b.Max.X; x++ {
src.SetRGBA(x, y, color.RGBA{
R: oc.src[i],
G: oc.src[i],
B: oc.src[i],
A: oc.src[i],
})
i++
}
}
return src
}
func (oc *transformOneColorTest) newDst() *image.RGBA {
return image.NewRGBA(image.Rect(0, 0, oc.dstWidth, oc.dstHeight))
}
func checkTransformTest(t *testing.T, oc *transformOneColorTest, dst *image.RGBA) bool {
for ch := 0; ch < 4; ch++ {
i := 0
res := make([]byte, len(oc.res))
for y := 0; y < oc.dstHeight; y++ {
for x := 0; x < oc.dstWidth; x++ {
off := (y-dst.Rect.Min.Y)*dst.Stride + (x-dst.Rect.Min.X)*4
res[i] = dst.Pix[off+ch]
i++
}
}
if !bytes.Equal(res, oc.res) {
got := graphicstest.SprintBox(res, oc.dstWidth, oc.dstHeight)
want := graphicstest.SprintBox(oc.res, oc.dstWidth, oc.dstHeight)
t.Errorf("%s: ch=%d\n got\n%s\n want\n%s", oc.desc, ch, got, want)
return false
}
}
return true
}

41
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,41 @@
// Copyright 2011 The Graphics-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 graphics
import (
"image"
"image/draw"
)
// Thumbnail scales and crops src so it fits in dst.
func Thumbnail(dst draw.Image, src image.Image) error {
// Scale down src in the dimension that is closer to dst.
sb := src.Bounds()
db := dst.Bounds()
rx := float64(sb.Dx()) / float64(db.Dx())
ry := float64(sb.Dy()) / float64(db.Dy())
var b image.Rectangle
if rx < ry {
b = image.Rect(0, 0, db.Dx(), int(float64(sb.Dy())/rx))
} else {
b = image.Rect(0, 0, int(float64(sb.Dx())/ry), db.Dy())
}
buf := image.NewRGBA(b)
if err := Scale(buf, src); err != nil {
return err
}
// Crop.
// TODO(crawshaw): improve on center-alignment.
var pt image.Point
if rx < ry {
pt.Y = (b.Dy() - db.Dy()) / 2
} else {
pt.X = (b.Dx() - db.Dx()) / 2
}
draw.Draw(dst, db, buf, pt, draw.Src)
return nil
}

53
Godeps/_workspace/src/code.google.com/p/graphics-go/graphics/thumbnail_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,53 @@
// Copyright 2011 The Graphics-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 graphics
import (
"code.google.com/p/graphics-go/graphics/graphicstest"
"image"
"testing"
_ "image/png"
)
func TestThumbnailGopher(t *testing.T) {
dst := image.NewRGBA(image.Rect(0, 0, 80, 80))
src, err := graphicstest.LoadImage("../testdata/gopher.png")
if err != nil {
t.Fatal(err)
}
if err := Thumbnail(dst, src); err != nil {
t.Fatal(err)
}
cmp, err := graphicstest.LoadImage("../testdata/gopher-thumb-80x80.png")
if err != nil {
t.Fatal(err)
}
err = graphicstest.ImageWithinTolerance(dst, cmp, 0)
if err != nil {
t.Error(err)
}
}
func TestThumbnailLongGopher(t *testing.T) {
dst := image.NewRGBA(image.Rect(0, 0, 50, 150))
src, err := graphicstest.LoadImage("../testdata/gopher.png")
if err != nil {
t.Fatal(err)
}
if err := Thumbnail(dst, src); err != nil {
t.Fatal(err)
}
cmp, err := graphicstest.LoadImage("../testdata/gopher-thumb-50x150.png")
if err != nil {
t.Fatal(err)
}
err = graphicstest.ImageWithinTolerance(dst, cmp, 0)
if err != nil {
t.Error(err)
}
}

4
Godeps/_workspace/src/github.com/rwcarlsen/goexif/exif/README.md сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,4 @@
To regenerate the regression test data, run `go generate` inside the exif
package directory and commit the changes to *regress_expected_test.go*.

42
Godeps/_workspace/src/github.com/rwcarlsen/goexif/exif/example_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,42 @@
package exif_test
import (
"fmt"
"log"
"os"
"github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/mknote"
)
func ExampleDecode() {
fname := "sample1.jpg"
f, err := os.Open(fname)
if err != nil {
log.Fatal(err)
}
// Optionally register camera makenote data parsing - currently Nikon and
// Canon are supported.
exif.RegisterParsers(mknote.All...)
x, err := exif.Decode(f)
if err != nil {
log.Fatal(err)
}
camModel, _ := x.Get(exif.Model) // normally, don't ignore errors!
fmt.Println(camModel.StringVal())
focal, _ := x.Get(exif.FocalLength)
numer, denom, _ := focal.Rat2(0) // retrieve first (only) rat. value
fmt.Printf("%v/%v", numer, denom)
// Two convenience functions exist for date/time taken and GPS coords:
tm, _ := x.DateTime()
fmt.Println("Taken: ", tm)
lat, long, _ := x.LatLong()
fmt.Println("lat, long: ", lat, ", ", long)
}

619
Godeps/_workspace/src/github.com/rwcarlsen/goexif/exif/exif.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,619 @@
// Package exif implements decoding of EXIF data as defined in the EXIF 2.2
// specification (http://www.exif.org/Exif2-2.PDF).
package exif
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"strconv"
"strings"
"time"
"github.com/rwcarlsen/goexif/tiff"
)
const (
jpeg_APP1 = 0xE1
exifPointer = 0x8769
gpsPointer = 0x8825
interopPointer = 0xA005
)
// A decodeError is returned when the image cannot be decoded as a tiff image.
type decodeError struct {
cause error
}
func (de decodeError) Error() string {
return fmt.Sprintf("exif: decode failed (%v) ", de.cause.Error())
}
// IsShortReadTagValueError identifies a ErrShortReadTagValue error.
func IsShortReadTagValueError(err error) bool {
de, ok := err.(decodeError)
if ok {
return de.cause == tiff.ErrShortReadTagValue
}
return false
}
// A TagNotPresentError is returned when the requested field is not
// present in the EXIF.
type TagNotPresentError FieldName
func (tag TagNotPresentError) Error() string {
return fmt.Sprintf("exif: tag %q is not present", string(tag))
}
func IsTagNotPresentError(err error) bool {
_, ok := err.(TagNotPresentError)
return ok
}
// Parser allows the registration of custom parsing and field loading
// in the Decode function.
type Parser interface {
// Parse should read data from x and insert parsed fields into x via
// LoadTags.
Parse(x *Exif) error
}
var parsers []Parser
func init() {
RegisterParsers(&parser{})
}
// RegisterParsers registers one or more parsers to be automatically called
// when decoding EXIF data via the Decode function.
func RegisterParsers(ps ...Parser) {
parsers = append(parsers, ps...)
}
type parser struct{}
type tiffErrors map[tiffError]string
func (te tiffErrors) Error() string {
var allErrors []string
for k, v := range te {
allErrors = append(allErrors, fmt.Sprintf("%s: %v\n", stagePrefix[k], v))
}
return strings.Join(allErrors, "\n")
}
// IsCriticalError, given the error returned by Decode, reports whether the
// returned *Exif may contain usable information.
func IsCriticalError(err error) bool {
_, ok := err.(tiffErrors)
return !ok
}
// IsExifError reports whether the error happened while decoding the EXIF
// sub-IFD.
func IsExifError(err error) bool {
if te, ok := err.(tiffErrors); ok {
_, isExif := te[loadExif]
return isExif
}
return false
}
// IsGPSError reports whether the error happened while decoding the GPS sub-IFD.
func IsGPSError(err error) bool {
if te, ok := err.(tiffErrors); ok {
_, isGPS := te[loadExif]
return isGPS
}
return false
}
// IsInteroperabilityError reports whether the error happened while decoding the
// Interoperability sub-IFD.
func IsInteroperabilityError(err error) bool {
if te, ok := err.(tiffErrors); ok {
_, isInterop := te[loadInteroperability]
return isInterop
}
return false
}
type tiffError int
const (
loadExif tiffError = iota
loadGPS
loadInteroperability
)
var stagePrefix = map[tiffError]string{
loadExif: "loading EXIF sub-IFD",
loadGPS: "loading GPS sub-IFD",
loadInteroperability: "loading Interoperability sub-IFD",
}
// Parse reads data from the tiff data in x and populates the tags
// in x. If parsing a sub-IFD fails, the error is recorded and
// parsing continues with the remaining sub-IFDs.
func (p *parser) Parse(x *Exif) error {
x.LoadTags(x.Tiff.Dirs[0], exifFields, false)
// thumbnails
if len(x.Tiff.Dirs) >= 2 {
x.LoadTags(x.Tiff.Dirs[1], thumbnailFields, false)
}
te := make(tiffErrors)
// recurse into exif, gps, and interop sub-IFDs
if err := loadSubDir(x, ExifIFDPointer, exifFields); err != nil {
te[loadExif] = err.Error()
}
if err := loadSubDir(x, GPSInfoIFDPointer, gpsFields); err != nil {
te[loadGPS] = err.Error()
}
if err := loadSubDir(x, InteroperabilityIFDPointer, interopFields); err != nil {
te[loadInteroperability] = err.Error()
}
if len(te) > 0 {
return te
}
return nil
}
func loadSubDir(x *Exif, ptr FieldName, fieldMap map[uint16]FieldName) error {
r := bytes.NewReader(x.Raw)
tag, err := x.Get(ptr)
if err != nil {
return nil
}
offset, err := tag.Int64(0)
if err != nil {
return nil
}
_, err = r.Seek(offset, 0)
if err != nil {
return fmt.Errorf("exif: seek to sub-IFD %s failed: %v", ptr, err)
}
subDir, _, err := tiff.DecodeDir(r, x.Tiff.Order)
if err != nil {
return fmt.Errorf("exif: sub-IFD %s decode failed: %v", ptr, err)
}
x.LoadTags(subDir, fieldMap, false)
return nil
}
// Exif provides access to decoded EXIF metadata fields and values.
type Exif struct {
Tiff *tiff.Tiff
main map[FieldName]*tiff.Tag
Raw []byte
}
// Decode parses EXIF-encoded data from r and returns a queryable Exif
// object. After the exif data section is called and the tiff structure
// decoded, each registered parser is called (in order of registration). If
// one parser returns an error, decoding terminates and the remaining
// parsers are not called.
// The error can be inspected with functions such as IsCriticalError to
// determine whether the returned object might still be usable.
func Decode(r io.Reader) (*Exif, error) {
// EXIF data in JPEG is stored in the APP1 marker. EXIF data uses the TIFF
// format to store data.
// If we're parsing a TIFF image, we don't need to strip away any data.
// If we're parsing a JPEG image, we need to strip away the JPEG APP1
// marker and also the EXIF header.
header := make([]byte, 4)
n, err := r.Read(header)
if err != nil {
return nil, err
}
if n < len(header) {
return nil, errors.New("exif: short read on header")
}
var isTiff bool
switch string(header) {
case "II*\x00":
// TIFF - Little endian (Intel)
isTiff = true
case "MM\x00*":
// TIFF - Big endian (Motorola)
isTiff = true
default:
// Not TIFF, assume JPEG
}
// Put the header bytes back into the reader.
r = io.MultiReader(bytes.NewReader(header), r)
var (
er *bytes.Reader
tif *tiff.Tiff
)
if isTiff {
// Functions below need the IFDs from the TIFF data to be stored in a
// *bytes.Reader. We use TeeReader to get a copy of the bytes as a
// side-effect of tiff.Decode() doing its work.
b := &bytes.Buffer{}
tr := io.TeeReader(r, b)
tif, err = tiff.Decode(tr)
er = bytes.NewReader(b.Bytes())
} else {
// Locate the JPEG APP1 header.
var sec *appSec
sec, err = newAppSec(jpeg_APP1, r)
if err != nil {
return nil, err
}
// Strip away EXIF header.
er, err = sec.exifReader()
if err != nil {
return nil, err
}
tif, err = tiff.Decode(er)
}
if err != nil {
return nil, decodeError{cause: err}
}
er.Seek(0, 0)
raw, err := ioutil.ReadAll(er)
if err != nil {
return nil, decodeError{cause: err}
}
// build an exif structure from the tiff
x := &Exif{
main: map[FieldName]*tiff.Tag{},
Tiff: tif,
Raw: raw,
}
for i, p := range parsers {
if err := p.Parse(x); err != nil {
if _, ok := err.(tiffErrors); ok {
return x, err
}
// This should never happen, as Parse always returns a tiffError
// for now, but that could change.
return x, fmt.Errorf("exif: parser %v failed (%v)", i, err)
}
}
return x, nil
}
// LoadTags loads tags into the available fields from the tiff Directory
// using the given tagid-fieldname mapping. Used to load makernote and
// other meta-data. If showMissing is true, tags in d that are not in the
// fieldMap will be loaded with the FieldName UnknownPrefix followed by the
// tag ID (in hex format).
func (x *Exif) LoadTags(d *tiff.Dir, fieldMap map[uint16]FieldName, showMissing bool) {
for _, tag := range d.Tags {
name := fieldMap[tag.Id]
if name == "" {
if !showMissing {
continue
}
name = FieldName(fmt.Sprintf("%v%x", UnknownPrefix, tag.Id))
}
x.main[name] = tag
}
}
// Get retrieves the EXIF tag for the given field name.
//
// If the tag is not known or not present, an error is returned. If the
// tag name is known, the error will be a TagNotPresentError.
func (x *Exif) Get(name FieldName) (*tiff.Tag, error) {
if tg, ok := x.main[name]; ok {
return tg, nil
}
return nil, TagNotPresentError(name)
}
// Walker is the interface used to traverse all fields of an Exif object.
type Walker interface {
// Walk is called for each non-nil EXIF field. Returning a non-nil
// error aborts the walk/traversal.
Walk(name FieldName, tag *tiff.Tag) error
}
// Walk calls the Walk method of w with the name and tag for every non-nil
// EXIF field. If w aborts the walk with an error, that error is returned.
func (x *Exif) Walk(w Walker) error {
for name, tag := range x.main {
if err := w.Walk(name, tag); err != nil {
return err
}
}
return nil
}
// DateTime returns the EXIF's "DateTimeOriginal" field, which
// is the creation time of the photo. If not found, it tries
// the "DateTime" (which is meant as the modtime) instead.
// The error will be TagNotPresentErr if none of those tags
// were found, or a generic error if the tag value was
// not a string, or the error returned by time.Parse.
//
// If the EXIF lacks timezone information or GPS time, the returned
// time's Location will be time.Local.
func (x *Exif) DateTime() (time.Time, error) {
var dt time.Time
tag, err := x.Get(DateTimeOriginal)
if err != nil {
tag, err = x.Get(DateTime)
if err != nil {
return dt, err
}
}
if tag.Format() != tiff.StringVal {
return dt, errors.New("DateTime[Original] not in string format")
}
exifTimeLayout := "2006:01:02 15:04:05"
dateStr := strings.TrimRight(string(tag.Val), "\x00")
// TODO(bradfitz,mpl): look for timezone offset, GPS time, etc.
// For now, just always return the time.Local timezone.
return time.ParseInLocation(exifTimeLayout, dateStr, time.Local)
}
func ratFloat(num, dem int64) float64 {
return float64(num) / float64(dem)
}
// Tries to parse a Geo degrees value from a string as it was found in some
// EXIF data.
// Supported formats so far:
// - "52,00000,50,00000,34,01180" ==> 52 deg 50'34.0118"
// Probably due to locale the comma is used as decimal mark as well as the
// separator of three floats (degrees, minutes, seconds)
// http://en.wikipedia.org/wiki/Decimal_mark#Hindu.E2.80.93Arabic_numeral_system
// - "52.0,50.0,34.01180" ==> 52deg50'34.0118"
// - "52,50,34.01180" ==> 52deg50'34.0118"
func parseTagDegreesString(s string) (float64, error) {
const unparsableErrorFmt = "Unknown coordinate format: %s"
isSplitRune := func(c rune) bool {
return c == ',' || c == ';'
}
parts := strings.FieldsFunc(s, isSplitRune)
var degrees, minutes, seconds float64
var err error
switch len(parts) {
case 6:
degrees, err = strconv.ParseFloat(parts[0]+"."+parts[1], 64)
if err != nil {
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
}
minutes, err = strconv.ParseFloat(parts[2]+"."+parts[3], 64)
if err != nil {
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
}
minutes = math.Copysign(minutes, degrees)
seconds, err = strconv.ParseFloat(parts[4]+"."+parts[5], 64)
if err != nil {
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
}
seconds = math.Copysign(seconds, degrees)
case 3:
degrees, err = strconv.ParseFloat(parts[0], 64)
if err != nil {
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
}
minutes, err = strconv.ParseFloat(parts[1], 64)
if err != nil {
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
}
minutes = math.Copysign(minutes, degrees)
seconds, err = strconv.ParseFloat(parts[2], 64)
if err != nil {
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
}
seconds = math.Copysign(seconds, degrees)
default:
return 0.0, fmt.Errorf(unparsableErrorFmt, s)
}
return degrees + minutes/60.0 + seconds/3600.0, nil
}
func parse3Rat2(tag *tiff.Tag) ([3]float64, error) {
v := [3]float64{}
for i := range v {
num, den, err := tag.Rat2(i)
if err != nil {
return v, err
}
v[i] = ratFloat(num, den)
if tag.Count < uint32(i+2) {
break
}
}
return v, nil
}
func tagDegrees(tag *tiff.Tag) (float64, error) {
switch tag.Format() {
case tiff.RatVal:
// The usual case, according to the Exif spec
// (http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf,
// sec 4.6.6, p. 52 et seq.)
v, err := parse3Rat2(tag)
if err != nil {
return 0.0, err
}
return v[0] + v[1]/60 + v[2]/3600.0, nil
case tiff.StringVal:
// Encountered this weird case with a panorama picture taken with a HTC phone
s, err := tag.StringVal()
if err != nil {
return 0.0, err
}
return parseTagDegreesString(s)
default:
// don't know how to parse value, give up
return 0.0, fmt.Errorf("Malformed EXIF Tag Degrees")
}
}
// LatLong returns the latitude and longitude of the photo and
// whether it was present.
func (x *Exif) LatLong() (lat, long float64, err error) {
// All calls of x.Get might return an TagNotPresentError
longTag, err := x.Get(FieldName("GPSLongitude"))
if err != nil {
return
}
ewTag, err := x.Get(FieldName("GPSLongitudeRef"))
if err != nil {
return
}
latTag, err := x.Get(FieldName("GPSLatitude"))
if err != nil {
return
}
nsTag, err := x.Get(FieldName("GPSLatitudeRef"))
if err != nil {
return
}
if long, err = tagDegrees(longTag); err != nil {
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
}
if lat, err = tagDegrees(latTag); err != nil {
return 0, 0, fmt.Errorf("Cannot parse latitude: %v", err)
}
ew, err := ewTag.StringVal()
if err == nil && ew == "W" {
long *= -1.0
} else if err != nil {
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
}
ns, err := nsTag.StringVal()
if err == nil && ns == "S" {
lat *= -1.0
} else if err != nil {
return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err)
}
return lat, long, nil
}
// String returns a pretty text representation of the decoded exif data.
func (x *Exif) String() string {
var buf bytes.Buffer
for name, tag := range x.main {
fmt.Fprintf(&buf, "%s: %s\n", name, tag)
}
return buf.String()
}
// JpegThumbnail returns the jpeg thumbnail if it exists. If it doesn't exist,
// TagNotPresentError will be returned
func (x *Exif) JpegThumbnail() ([]byte, error) {
offset, err := x.Get(ThumbJPEGInterchangeFormat)
if err != nil {
return nil, err
}
start, err := offset.Int(0)
if err != nil {
return nil, err
}
length, err := x.Get(ThumbJPEGInterchangeFormatLength)
if err != nil {
return nil, err
}
l, err := length.Int(0)
if err != nil {
return nil, err
}
return x.Raw[start : start+l], nil
}
// MarshalJson implements the encoding/json.Marshaler interface providing output of
// all EXIF fields present (names and values).
func (x Exif) MarshalJSON() ([]byte, error) {
return json.Marshal(x.main)
}
type appSec struct {
marker byte
data []byte
}
// newAppSec finds marker in r and returns the corresponding application data
// section.
func newAppSec(marker byte, r io.Reader) (*appSec, error) {
br := bufio.NewReader(r)
app := &appSec{marker: marker}
var dataLen int
// seek to marker
for dataLen == 0 {
if _, err := br.ReadBytes(0xFF); err != nil {
return nil, err
}
c, err := br.ReadByte()
if err != nil {
return nil, err
} else if c != marker {
continue
}
dataLenBytes := make([]byte, 2)
for k,_ := range dataLenBytes {
c, err := br.ReadByte()
if err != nil {
return nil, err
}
dataLenBytes[k] = c
}
dataLen = int(binary.BigEndian.Uint16(dataLenBytes)) - 2
}
// read section data
nread := 0
for nread < dataLen {
s := make([]byte, dataLen-nread)
n, err := br.Read(s)
nread += n
if err != nil && nread < dataLen {
return nil, err
}
app.data = append(app.data, s[:n]...)
}
return app, nil
}
// reader returns a reader on this appSec.
func (app *appSec) reader() *bytes.Reader {
return bytes.NewReader(app.data)
}
// exifReader returns a reader on this appSec with the read cursor advanced to
// the start of the exif's tiff encoded portion.
func (app *appSec) exifReader() (*bytes.Reader, error) {
if len(app.data) < 6 {
return nil, errors.New("exif: failed to find exif intro marker")
}
// read/check for exif special mark
exif := app.data[:6]
if !bytes.Equal(exif, append([]byte("Exif"), 0x00, 0x00)) {
return nil, errors.New("exif: failed to find exif intro marker")
}
return bytes.NewReader(app.data[6:]), nil
}

202
Godeps/_workspace/src/github.com/rwcarlsen/goexif/exif/exif_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,202 @@
package exif
//go:generate go run regen_regress.go -- regress_expected_test.go
//go:generate go fmt regress_expected_test.go
import (
"flag"
"fmt"
"math"
"os"
"path/filepath"
"strings"
"testing"
"github.com/rwcarlsen/goexif/tiff"
)
var dataDir = flag.String("test_data_dir", ".", "Directory where the data files for testing are located")
func TestDecode(t *testing.T) {
fpath := filepath.Join(*dataDir, "samples")
f, err := os.Open(fpath)
if err != nil {
t.Fatalf("Could not open sample directory '%s': %v", fpath, err)
}
names, err := f.Readdirnames(0)
if err != nil {
t.Fatalf("Could not read sample directory '%s': %v", fpath, err)
}
cnt := 0
for _, name := range names {
if !strings.HasSuffix(name, ".jpg") {
t.Logf("skipping non .jpg file %v", name)
continue
}
t.Logf("testing file %v", name)
f, err := os.Open(filepath.Join(fpath, name))
if err != nil {
t.Fatal(err)
}
x, err := Decode(f)
if err != nil {
t.Fatal(err)
} else if x == nil {
t.Fatalf("No error and yet %v was not decoded", name)
}
t.Logf("checking pic %v", name)
x.Walk(&walker{name, t})
cnt++
}
if cnt != len(regressExpected) {
t.Errorf("Did not process enough samples, got %d, want %d", cnt, len(regressExpected))
}
}
type walker struct {
picName string
t *testing.T
}
func (w *walker) Walk(field FieldName, tag *tiff.Tag) error {
// this needs to be commented out when regenerating regress expected vals
pic := regressExpected[w.picName]
if pic == nil {
w.t.Errorf(" regression data not found")
return nil
}
exp, ok := pic[field]
if !ok {
w.t.Errorf(" regression data does not have field %v", field)
return nil
}
s := tag.String()
if tag.Count == 1 && s != "\"\"" {
s = fmt.Sprintf("[%s]", s)
}
got := tag.String()
if exp != got {
fmt.Println("s: ", s)
fmt.Printf("len(s)=%v\n", len(s))
w.t.Errorf(" field %v bad tag: expected '%s', got '%s'", field, exp, got)
}
return nil
}
func TestMarshal(t *testing.T) {
name := filepath.Join(*dataDir, "sample1.jpg")
f, err := os.Open(name)
if err != nil {
t.Fatalf("%v\n", err)
}
defer f.Close()
x, err := Decode(f)
if err != nil {
t.Fatal(err)
}
if x == nil {
t.Fatal("bad err")
}
b, err := x.MarshalJSON()
if err != nil {
t.Fatal(err)
}
t.Logf("%s", b)
}
func testSingleParseDegreesString(t *testing.T, s string, w float64) {
g, err := parseTagDegreesString(s)
if err != nil {
t.Fatal(err)
}
if math.Abs(w-g) > 1e-10 {
t.Errorf("Wrong parsing result %s: Want %.12f, got %.12f", s, w, g)
}
}
func TestParseTagDegreesString(t *testing.T) {
// semicolon as decimal mark
testSingleParseDegreesString(t, "52,00000,50,00000,34,01180", 52.842781055556) // comma as separator
testSingleParseDegreesString(t, "52,00000;50,00000;34,01180", 52.842781055556) // semicolon as separator
// point as decimal mark
testSingleParseDegreesString(t, "14.00000,44.00000,34.01180", 14.742781055556) // comma as separator
testSingleParseDegreesString(t, "14.00000;44.00000;34.01180", 14.742781055556) // semicolon as separator
testSingleParseDegreesString(t, "14.00000;44.00000,34.01180", 14.742781055556) // mixed separators
testSingleParseDegreesString(t, "-008.0,30.0,03.6", -8.501) // leading zeros
// no decimal places
testSingleParseDegreesString(t, "-10,15,54", -10.265)
testSingleParseDegreesString(t, "-10;15;54", -10.265)
// incorrect mix of comma and point as decimal mark
s := "-17,00000,15.00000,04.80000"
if _, err := parseTagDegreesString(s); err == nil {
t.Error("parseTagDegreesString: false positive for " + s)
}
}
// Make sure we error out early when a tag had a count of MaxUint32
func TestMaxUint32CountError(t *testing.T) {
name := filepath.Join(*dataDir, "corrupt/max_uint32_exif.jpg")
f, err := os.Open(name)
if err != nil {
t.Fatalf("%v\n", err)
}
defer f.Close()
_, err = Decode(f)
if err == nil {
t.Fatal("no error on bad exif data")
}
if !strings.Contains(err.Error(), "invalid Count offset") {
t.Fatal("wrong error:", err.Error())
}
}
// Make sure we error out early with tag data sizes larger than the image file
func TestHugeTagError(t *testing.T) {
name := filepath.Join(*dataDir, "corrupt/huge_tag_exif.jpg")
f, err := os.Open(name)
if err != nil {
t.Fatalf("%v\n", err)
}
defer f.Close()
_, err = Decode(f)
if err == nil {
t.Fatal("no error on bad exif data")
}
if !strings.Contains(err.Error(), "short read") {
t.Fatal("wrong error:", err.Error())
}
}
// Check for a 0-length tag value
func TestZeroLengthTagError(t *testing.T) {
name := filepath.Join(*dataDir, "corrupt/infinite_loop_exif.jpg")
f, err := os.Open(name)
if err != nil {
t.Fatalf("%v\n", err)
}
defer f.Close()
_, err = Decode(f)
if err == nil {
t.Fatal("no error on bad exif data")
}
if !strings.Contains(err.Error(), "zero length tag value") {
t.Fatal("wrong error:", err.Error())
}
}

293
Godeps/_workspace/src/github.com/rwcarlsen/goexif/exif/fields.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,293 @@
package exif
type FieldName string
// UnknownPrefix is used as the first part of field names for decoded tags for
// which there is no known/supported EXIF field.
const UnknownPrefix = "UnknownTag_"
// Primary EXIF fields
const (
ImageWidth FieldName = "ImageWidth"
ImageLength = "ImageLength" // Image height called Length by EXIF spec
BitsPerSample = "BitsPerSample"
Compression = "Compression"
PhotometricInterpretation = "PhotometricInterpretation"
Orientation = "Orientation"
SamplesPerPixel = "SamplesPerPixel"
PlanarConfiguration = "PlanarConfiguration"
YCbCrSubSampling = "YCbCrSubSampling"
YCbCrPositioning = "YCbCrPositioning"
XResolution = "XResolution"
YResolution = "YResolution"
ResolutionUnit = "ResolutionUnit"
DateTime = "DateTime"
ImageDescription = "ImageDescription"
Make = "Make"
Model = "Model"
Software = "Software"
Artist = "Artist"
Copyright = "Copyright"
ExifIFDPointer = "ExifIFDPointer"
GPSInfoIFDPointer = "GPSInfoIFDPointer"
InteroperabilityIFDPointer = "InteroperabilityIFDPointer"
ExifVersion = "ExifVersion"
FlashpixVersion = "FlashpixVersion"
ColorSpace = "ColorSpace"
ComponentsConfiguration = "ComponentsConfiguration"
CompressedBitsPerPixel = "CompressedBitsPerPixel"
PixelXDimension = "PixelXDimension"
PixelYDimension = "PixelYDimension"
MakerNote = "MakerNote"
UserComment = "UserComment"
RelatedSoundFile = "RelatedSoundFile"
DateTimeOriginal = "DateTimeOriginal"
DateTimeDigitized = "DateTimeDigitized"
SubSecTime = "SubSecTime"
SubSecTimeOriginal = "SubSecTimeOriginal"
SubSecTimeDigitized = "SubSecTimeDigitized"
ImageUniqueID = "ImageUniqueID"
ExposureTime = "ExposureTime"
FNumber = "FNumber"
ExposureProgram = "ExposureProgram"
SpectralSensitivity = "SpectralSensitivity"
ISOSpeedRatings = "ISOSpeedRatings"
OECF = "OECF"
ShutterSpeedValue = "ShutterSpeedValue"
ApertureValue = "ApertureValue"
BrightnessValue = "BrightnessValue"
ExposureBiasValue = "ExposureBiasValue"
MaxApertureValue = "MaxApertureValue"
SubjectDistance = "SubjectDistance"
MeteringMode = "MeteringMode"
LightSource = "LightSource"
Flash = "Flash"
FocalLength = "FocalLength"
SubjectArea = "SubjectArea"
FlashEnergy = "FlashEnergy"
SpatialFrequencyResponse = "SpatialFrequencyResponse"
FocalPlaneXResolution = "FocalPlaneXResolution"
FocalPlaneYResolution = "FocalPlaneYResolution"
FocalPlaneResolutionUnit = "FocalPlaneResolutionUnit"
SubjectLocation = "SubjectLocation"
ExposureIndex = "ExposureIndex"
SensingMethod = "SensingMethod"
FileSource = "FileSource"
SceneType = "SceneType"
CFAPattern = "CFAPattern"
CustomRendered = "CustomRendered"
ExposureMode = "ExposureMode"
WhiteBalance = "WhiteBalance"
DigitalZoomRatio = "DigitalZoomRatio"
FocalLengthIn35mmFilm = "FocalLengthIn35mmFilm"
SceneCaptureType = "SceneCaptureType"
GainControl = "GainControl"
Contrast = "Contrast"
Saturation = "Saturation"
Sharpness = "Sharpness"
DeviceSettingDescription = "DeviceSettingDescription"
SubjectDistanceRange = "SubjectDistanceRange"
LensMake = "LensMake"
LensModel = "LensModel"
)
// thumbnail fields
const (
ThumbJPEGInterchangeFormat = "ThumbJPEGInterchangeFormat" // offset to thumb jpeg SOI
ThumbJPEGInterchangeFormatLength = "ThumbJPEGInterchangeFormatLength" // byte length of thumb
)
// GPS fields
const (
GPSVersionID FieldName = "GPSVersionID"
GPSLatitudeRef = "GPSLatitudeRef"
GPSLatitude = "GPSLatitude"
GPSLongitudeRef = "GPSLongitudeRef"
GPSLongitude = "GPSLongitude"
GPSAltitudeRef = "GPSAltitudeRef"
GPSAltitude = "GPSAltitude"
GPSTimeStamp = "GPSTimeStamp"
GPSSatelites = "GPSSatelites"
GPSStatus = "GPSStatus"
GPSMeasureMode = "GPSMeasureMode"
GPSDOP = "GPSDOP"
GPSSpeedRef = "GPSSpeedRef"
GPSSpeed = "GPSSpeed"
GPSTrackRef = "GPSTrackRef"
GPSTrack = "GPSTrack"
GPSImgDirectionRef = "GPSImgDirectionRef"
GPSImgDirection = "GPSImgDirection"
GPSMapDatum = "GPSMapDatum"
GPSDestLatitudeRef = "GPSDestLatitudeRef"
GPSDestLatitude = "GPSDestLatitude"
GPSDestLongitudeRef = "GPSDestLongitudeRef"
GPSDestLongitude = "GPSDestLongitude"
GPSDestBearingRef = "GPSDestBearingRef"
GPSDestBearing = "GPSDestBearing"
GPSDestDistanceRef = "GPSDestDistanceRef"
GPSDestDistance = "GPSDestDistance"
GPSProcessingMethod = "GPSProcessingMethod"
GPSAreaInformation = "GPSAreaInformation"
GPSDateStamp = "GPSDateStamp"
GPSDifferential = "GPSDifferential"
)
// interoperability fields
const (
InteroperabilityIndex FieldName = "InteroperabilityIndex"
)
var exifFields = map[uint16]FieldName{
/////////////////////////////////////
////////// IFD 0 ////////////////////
/////////////////////////////////////
// image data structure for the thumbnail
0x0100: ImageWidth,
0x0101: ImageLength,
0x0102: BitsPerSample,
0x0103: Compression,
0x0106: PhotometricInterpretation,
0x0112: Orientation,
0x0115: SamplesPerPixel,
0x011C: PlanarConfiguration,
0x0212: YCbCrSubSampling,
0x0213: YCbCrPositioning,
0x011A: XResolution,
0x011B: YResolution,
0x0128: ResolutionUnit,
// Other tags
0x0132: DateTime,
0x010E: ImageDescription,
0x010F: Make,
0x0110: Model,
0x0131: Software,
0x013B: Artist,
0x8298: Copyright,
// private tags
exifPointer: ExifIFDPointer,
/////////////////////////////////////
////////// Exif sub IFD /////////////
/////////////////////////////////////
gpsPointer: GPSInfoIFDPointer,
interopPointer: InteroperabilityIFDPointer,
0x9000: ExifVersion,
0xA000: FlashpixVersion,
0xA001: ColorSpace,
0x9101: ComponentsConfiguration,
0x9102: CompressedBitsPerPixel,
0xA002: PixelXDimension,
0xA003: PixelYDimension,
0x927C: MakerNote,
0x9286: UserComment,
0xA004: RelatedSoundFile,
0x9003: DateTimeOriginal,
0x9004: DateTimeDigitized,
0x9290: SubSecTime,
0x9291: SubSecTimeOriginal,
0x9292: SubSecTimeDigitized,
0xA420: ImageUniqueID,
// picture conditions
0x829A: ExposureTime,
0x829D: FNumber,
0x8822: ExposureProgram,
0x8824: SpectralSensitivity,
0x8827: ISOSpeedRatings,
0x8828: OECF,
0x9201: ShutterSpeedValue,
0x9202: ApertureValue,
0x9203: BrightnessValue,
0x9204: ExposureBiasValue,
0x9205: MaxApertureValue,
0x9206: SubjectDistance,
0x9207: MeteringMode,
0x9208: LightSource,
0x9209: Flash,
0x920A: FocalLength,
0x9214: SubjectArea,
0xA20B: FlashEnergy,
0xA20C: SpatialFrequencyResponse,
0xA20E: FocalPlaneXResolution,
0xA20F: FocalPlaneYResolution,
0xA210: FocalPlaneResolutionUnit,
0xA214: SubjectLocation,
0xA215: ExposureIndex,
0xA217: SensingMethod,
0xA300: FileSource,
0xA301: SceneType,
0xA302: CFAPattern,
0xA401: CustomRendered,
0xA402: ExposureMode,
0xA403: WhiteBalance,
0xA404: DigitalZoomRatio,
0xA405: FocalLengthIn35mmFilm,
0xA406: SceneCaptureType,
0xA407: GainControl,
0xA408: Contrast,
0xA409: Saturation,
0xA40A: Sharpness,
0xA40B: DeviceSettingDescription,
0xA40C: SubjectDistanceRange,
0xA433: LensMake,
0xA434: LensModel,
}
var gpsFields = map[uint16]FieldName{
/////////////////////////////////////
//// GPS sub-IFD ////////////////////
/////////////////////////////////////
0x0: GPSVersionID,
0x1: GPSLatitudeRef,
0x2: GPSLatitude,
0x3: GPSLongitudeRef,
0x4: GPSLongitude,
0x5: GPSAltitudeRef,
0x6: GPSAltitude,
0x7: GPSTimeStamp,
0x8: GPSSatelites,
0x9: GPSStatus,
0xA: GPSMeasureMode,
0xB: GPSDOP,
0xC: GPSSpeedRef,
0xD: GPSSpeed,
0xE: GPSTrackRef,
0xF: GPSTrack,
0x10: GPSImgDirectionRef,
0x11: GPSImgDirection,
0x12: GPSMapDatum,
0x13: GPSDestLatitudeRef,
0x14: GPSDestLatitude,
0x15: GPSDestLongitudeRef,
0x16: GPSDestLongitude,
0x17: GPSDestBearingRef,
0x18: GPSDestBearing,
0x19: GPSDestDistanceRef,
0x1A: GPSDestDistance,
0x1B: GPSProcessingMethod,
0x1C: GPSAreaInformation,
0x1D: GPSDateStamp,
0x1E: GPSDifferential,
}
var interopFields = map[uint16]FieldName{
/////////////////////////////////////
//// Interoperability sub-IFD ///////
/////////////////////////////////////
0x1: InteroperabilityIndex,
}
var thumbnailFields = map[uint16]FieldName{
0x0201: ThumbJPEGInterchangeFormat,
0x0202: ThumbJPEGInterchangeFormatLength,
}

79
Godeps/_workspace/src/github.com/rwcarlsen/goexif/exif/regen_regress.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,79 @@
// +build ignore
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/tiff"
)
func main() {
flag.Parse()
fname := flag.Arg(0)
dst, err := os.Create(fname)
if err != nil {
log.Fatal(err)
}
defer dst.Close()
dir, err := os.Open("samples")
if err != nil {
log.Fatal(err)
}
defer dir.Close()
names, err := dir.Readdirnames(0)
if err != nil {
log.Fatal(err)
}
for i, name := range names {
names[i] = filepath.Join("samples", name)
}
makeExpected(names, dst)
}
func makeExpected(files []string, w io.Writer) {
fmt.Fprintf(w, "package exif\n\n")
fmt.Fprintf(w, "var regressExpected = map[string]map[FieldName]string{\n")
for _, name := range files {
f, err := os.Open(name)
if err != nil {
continue
}
x, err := exif.Decode(f)
if err != nil {
f.Close()
continue
}
fmt.Fprintf(w, "\"%v\": map[FieldName]string{\n", filepath.Base(name))
x.Walk(&regresswalk{w})
fmt.Fprintf(w, "},\n")
f.Close()
}
fmt.Fprintf(w, "}")
}
type regresswalk struct {
wr io.Writer
}
func (w *regresswalk) Walk(name exif.FieldName, tag *tiff.Tag) error {
if strings.HasPrefix(string(name), exif.UnknownPrefix) {
fmt.Fprintf(w.wr, "\"%v\": `%v`,\n", name, tag.String())
} else {
fmt.Fprintf(w.wr, "%v: `%v`,\n", name, tag.String())
}
return nil
}

2293
Godeps/_workspace/src/github.com/rwcarlsen/goexif/exif/regress_expected_test.go сгенерированный поставляемый Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

438
Godeps/_workspace/src/github.com/rwcarlsen/goexif/tiff/tag.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,438 @@
package tiff
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"strings"
"unicode"
"unicode/utf8"
)
// Format specifies the Go type equivalent used to represent the basic
// tiff data types.
type Format int
const (
IntVal Format = iota
FloatVal
RatVal
StringVal
UndefVal
OtherVal
)
var ErrShortReadTagValue = errors.New("tiff: short read of tag value")
var formatNames = map[Format]string{
IntVal: "int",
FloatVal: "float",
RatVal: "rational",
StringVal: "string",
UndefVal: "undefined",
OtherVal: "other",
}
// DataType represents the basic tiff tag data types.
type DataType uint16
const (
DTByte DataType = 1
DTAscii = 2
DTShort = 3
DTLong = 4
DTRational = 5
DTSByte = 6
DTUndefined = 7
DTSShort = 8
DTSLong = 9
DTSRational = 10
DTFloat = 11
DTDouble = 12
)
var typeNames = map[DataType]string{
DTByte: "byte",
DTAscii: "ascii",
DTShort: "short",
DTLong: "long",
DTRational: "rational",
DTSByte: "signed byte",
DTUndefined: "undefined",
DTSShort: "signed short",
DTSLong: "signed long",
DTSRational: "signed rational",
DTFloat: "float",
DTDouble: "double",
}
// typeSize specifies the size in bytes of each type.
var typeSize = map[DataType]uint32{
DTByte: 1,
DTAscii: 1,
DTShort: 2,
DTLong: 4,
DTRational: 8,
DTSByte: 1,
DTUndefined: 1,
DTSShort: 2,
DTSLong: 4,
DTSRational: 8,
DTFloat: 4,
DTDouble: 8,
}
// Tag reflects the parsed content of a tiff IFD tag.
type Tag struct {
// Id is the 2-byte tiff tag identifier.
Id uint16
// Type is an integer (1 through 12) indicating the tag value's data type.
Type DataType
// Count is the number of type Type stored in the tag's value (i.e. the
// tag's value is an array of type Type and length Count).
Count uint32
// Val holds the bytes that represent the tag's value.
Val []byte
// ValOffset holds byte offset of the tag value w.r.t. the beginning of the
// reader it was decoded from. Zero if the tag value fit inside the offset
// field.
ValOffset uint32
order binary.ByteOrder
intVals []int64
floatVals []float64
ratVals [][]int64
strVal string
format Format
}
// DecodeTag parses a tiff-encoded IFD tag from r and returns a Tag object. The
// first read from r should be the first byte of the tag. ReadAt offsets should
// generally be relative to the beginning of the tiff structure (not relative
// to the beginning of the tag).
func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) {
t := new(Tag)
t.order = order
err := binary.Read(r, order, &t.Id)
if err != nil {
return nil, errors.New("tiff: tag id read failed: " + err.Error())
}
err = binary.Read(r, order, &t.Type)
if err != nil {
return nil, errors.New("tiff: tag type read failed: " + err.Error())
}
err = binary.Read(r, order, &t.Count)
if err != nil {
return nil, errors.New("tiff: tag component count read failed: " + err.Error())
}
// There seems to be a relatively common corrupt tag which has a Count of
// MaxUint32. This is probably not a valid value, so return early.
if t.Count == 1<<32-1 {
return t, errors.New("invalid Count offset in tag")
}
valLen := typeSize[t.Type] * t.Count
if valLen == 0 {
return t, errors.New("zero length tag value")
}
if valLen > 4 {
binary.Read(r, order, &t.ValOffset)
// Use a bytes.Buffer so we don't allocate a huge slice if the tag
// is corrupt.
var buff bytes.Buffer
sr := io.NewSectionReader(r, int64(t.ValOffset), int64(valLen))
n, err := io.Copy(&buff, sr)
if err != nil {
return t, errors.New("tiff: tag value read failed: " + err.Error())
} else if n != int64(valLen) {
return t, ErrShortReadTagValue
}
t.Val = buff.Bytes()
} else {
val := make([]byte, valLen)
if _, err = io.ReadFull(r, val); err != nil {
return t, errors.New("tiff: tag offset read failed: " + err.Error())
}
// ignore padding.
if _, err = io.ReadFull(r, make([]byte, 4-valLen)); err != nil {
return t, errors.New("tiff: tag offset read failed: " + err.Error())
}
t.Val = val
}
return t, t.convertVals()
}
func (t *Tag) convertVals() error {
r := bytes.NewReader(t.Val)
switch t.Type {
case DTAscii:
if len(t.Val) > 0 {
t.strVal = string(t.Val[:len(t.Val)-1]) // ignore the last byte (NULL).
}
case DTByte:
var v uint8
t.intVals = make([]int64, int(t.Count))
for i := range t.intVals {
err := binary.Read(r, t.order, &v)
if err != nil {
return err
}
t.intVals[i] = int64(v)
}
case DTShort:
var v uint16
t.intVals = make([]int64, int(t.Count))
for i := range t.intVals {
err := binary.Read(r, t.order, &v)
if err != nil {
return err
}
t.intVals[i] = int64(v)
}
case DTLong:
var v uint32
t.intVals = make([]int64, int(t.Count))
for i := range t.intVals {
err := binary.Read(r, t.order, &v)
if err != nil {
return err
}
t.intVals[i] = int64(v)
}
case DTSByte:
var v int8
t.intVals = make([]int64, int(t.Count))
for i := range t.intVals {
err := binary.Read(r, t.order, &v)
if err != nil {
return err
}
t.intVals[i] = int64(v)
}
case DTSShort:
var v int16
t.intVals = make([]int64, int(t.Count))
for i := range t.intVals {
err := binary.Read(r, t.order, &v)
if err != nil {
return err
}
t.intVals[i] = int64(v)
}
case DTSLong:
var v int32
t.intVals = make([]int64, int(t.Count))
for i := range t.intVals {
err := binary.Read(r, t.order, &v)
if err != nil {
return err
}
t.intVals[i] = int64(v)
}
case DTRational:
t.ratVals = make([][]int64, int(t.Count))
for i := range t.ratVals {
var n, d uint32
err := binary.Read(r, t.order, &n)
if err != nil {
return err
}
err = binary.Read(r, t.order, &d)
if err != nil {
return err
}
t.ratVals[i] = []int64{int64(n), int64(d)}
}
case DTSRational:
t.ratVals = make([][]int64, int(t.Count))
for i := range t.ratVals {
var n, d int32
err := binary.Read(r, t.order, &n)
if err != nil {
return err
}
err = binary.Read(r, t.order, &d)
if err != nil {
return err
}
t.ratVals[i] = []int64{int64(n), int64(d)}
}
case DTFloat: // float32
t.floatVals = make([]float64, int(t.Count))
for i := range t.floatVals {
var v float32
err := binary.Read(r, t.order, &v)
if err != nil {
return err
}
t.floatVals[i] = float64(v)
}
case DTDouble:
t.floatVals = make([]float64, int(t.Count))
for i := range t.floatVals {
var u float64
err := binary.Read(r, t.order, &u)
if err != nil {
return err
}
t.floatVals[i] = u
}
}
switch t.Type {
case DTByte, DTShort, DTLong, DTSByte, DTSShort, DTSLong:
t.format = IntVal
case DTRational, DTSRational:
t.format = RatVal
case DTFloat, DTDouble:
t.format = FloatVal
case DTAscii:
t.format = StringVal
case DTUndefined:
t.format = UndefVal
default:
t.format = OtherVal
}
return nil
}
// Format returns a value indicating which method can be called to retrieve the
// tag's value properly typed (e.g. integer, rational, etc.).
func (t *Tag) Format() Format { return t.format }
func (t *Tag) typeErr(to Format) error {
return &wrongFmtErr{typeNames[t.Type], formatNames[to]}
}
// Rat returns the tag's i'th value as a rational number. It returns a nil and
// an error if this tag's Format is not RatVal. It panics for zero deminators
// or if i is out of range.
func (t *Tag) Rat(i int) (*big.Rat, error) {
n, d, err := t.Rat2(i)
if err != nil {
return nil, err
}
return big.NewRat(n, d), nil
}
// Rat2 returns the tag's i'th value as a rational number represented by a
// numerator-denominator pair. It returns an error if the tag's Format is not
// RatVal. It panics if i is out of range.
func (t *Tag) Rat2(i int) (num, den int64, err error) {
if t.format != RatVal {
return 0, 0, t.typeErr(RatVal)
}
return t.ratVals[i][0], t.ratVals[i][1], nil
}
// Int64 returns the tag's i'th value as an integer. It returns an error if the
// tag's Format is not IntVal. It panics if i is out of range.
func (t *Tag) Int64(i int) (int64, error) {
if t.format != IntVal {
return 0, t.typeErr(IntVal)
}
return t.intVals[i], nil
}
// Int returns the tag's i'th value as an integer. It returns an error if the
// tag's Format is not IntVal. It panics if i is out of range.
func (t *Tag) Int(i int) (int, error) {
if t.format != IntVal {
return 0, t.typeErr(IntVal)
}
return int(t.intVals[i]), nil
}
// Float returns the tag's i'th value as a float. It returns an error if the
// tag's Format is not IntVal. It panics if i is out of range.
func (t *Tag) Float(i int) (float64, error) {
if t.format != FloatVal {
return 0, t.typeErr(FloatVal)
}
return t.floatVals[i], nil
}
// StringVal returns the tag's value as a string. It returns an error if the
// tag's Format is not StringVal. It panics if i is out of range.
func (t *Tag) StringVal() (string, error) {
if t.format != StringVal {
return "", t.typeErr(StringVal)
}
return t.strVal, nil
}
// String returns a nicely formatted version of the tag.
func (t *Tag) String() string {
data, err := t.MarshalJSON()
if err != nil {
return "ERROR: " + err.Error()
}
if t.Count == 1 {
return strings.Trim(fmt.Sprintf("%s", data), "[]")
}
return fmt.Sprintf("%s", data)
}
func (t *Tag) MarshalJSON() ([]byte, error) {
switch t.format {
case StringVal, UndefVal:
return nullString(t.Val), nil
case OtherVal:
return []byte(fmt.Sprintf("unknown tag type '%v'", t.Type)), nil
}
rv := []string{}
for i := 0; i < int(t.Count); i++ {
switch t.format {
case RatVal:
n, d, _ := t.Rat2(i)
rv = append(rv, fmt.Sprintf(`"%v/%v"`, n, d))
case FloatVal:
v, _ := t.Float(i)
rv = append(rv, fmt.Sprintf("%v", v))
case IntVal:
v, _ := t.Int(i)
rv = append(rv, fmt.Sprintf("%v", v))
}
}
return []byte(fmt.Sprintf(`[%s]`, strings.Join(rv, ","))), nil
}
func nullString(in []byte) []byte {
rv := bytes.Buffer{}
rv.WriteByte('"')
for _, b := range in {
if unicode.IsPrint(rune(b)) {
rv.WriteByte(b)
}
}
rv.WriteByte('"')
rvb := rv.Bytes()
if utf8.Valid(rvb) {
return rvb
}
return []byte(`""`)
}
type wrongFmtErr struct {
From, To string
}
func (e *wrongFmtErr) Error() string {
return fmt.Sprintf("cannot convert tag type '%v' into '%v'", e.From, e.To)
}

153
Godeps/_workspace/src/github.com/rwcarlsen/goexif/tiff/tiff.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,153 @@
// Package tiff implements TIFF decoding as defined in TIFF 6.0 specification at
// http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf
package tiff
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
)
// ReadAtReader is used when decoding Tiff tags and directories
type ReadAtReader interface {
io.Reader
io.ReaderAt
}
// Tiff provides access to a decoded tiff data structure.
type Tiff struct {
// Dirs is an ordered slice of the tiff's Image File Directories (IFDs).
// The IFD at index 0 is IFD0.
Dirs []*Dir
// The tiff's byte-encoding (i.e. big/little endian).
Order binary.ByteOrder
}
// Decode parses tiff-encoded data from r and returns a Tiff struct that
// reflects the structure and content of the tiff data. The first read from r
// should be the first byte of the tiff-encoded data and not necessarily the
// first byte of an os.File object.
func Decode(r io.Reader) (*Tiff, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return nil, errors.New("tiff: could not read data")
}
buf := bytes.NewReader(data)
t := new(Tiff)
// read byte order
bo := make([]byte, 2)
if _, err = io.ReadFull(buf, bo); err != nil {
return nil, errors.New("tiff: could not read tiff byte order")
}
if string(bo) == "II" {
t.Order = binary.LittleEndian
} else if string(bo) == "MM" {
t.Order = binary.BigEndian
} else {
return nil, errors.New("tiff: could not read tiff byte order")
}
// check for special tiff marker
var sp int16
err = binary.Read(buf, t.Order, &sp)
if err != nil || 42 != sp {
return nil, errors.New("tiff: could not find special tiff marker")
}
// load offset to first IFD
var offset int32
err = binary.Read(buf, t.Order, &offset)
if err != nil {
return nil, errors.New("tiff: could not read offset to first IFD")
}
// load IFD's
var d *Dir
prev := offset
for offset != 0 {
// seek to offset
_, err := buf.Seek(int64(offset), 0)
if err != nil {
return nil, errors.New("tiff: seek to IFD failed")
}
if buf.Len() == 0 {
return nil, errors.New("tiff: seek offset after EOF")
}
// load the dir
d, offset, err = DecodeDir(buf, t.Order)
if err != nil {
return nil, err
}
if offset == prev {
return nil, errors.New("tiff: recursive IFD")
}
prev = offset
t.Dirs = append(t.Dirs, d)
}
return t, nil
}
func (tf *Tiff) String() string {
var buf bytes.Buffer
fmt.Fprint(&buf, "Tiff{")
for _, d := range tf.Dirs {
fmt.Fprintf(&buf, "%s, ", d.String())
}
fmt.Fprintf(&buf, "}")
return buf.String()
}
// Dir provides access to the parsed content of a tiff Image File Directory (IFD).
type Dir struct {
Tags []*Tag
}
// DecodeDir parses a tiff-encoded IFD from r and returns a Dir object. offset
// is the offset to the next IFD. The first read from r should be at the first
// byte of the IFD. ReadAt offsets should generally be relative to the
// beginning of the tiff structure (not relative to the beginning of the IFD).
func DecodeDir(r ReadAtReader, order binary.ByteOrder) (d *Dir, offset int32, err error) {
d = new(Dir)
// get num of tags in ifd
var nTags int16
err = binary.Read(r, order, &nTags)
if err != nil {
return nil, 0, errors.New("tiff: failed to read IFD tag count: " + err.Error())
}
// load tags
for n := 0; n < int(nTags); n++ {
t, err := DecodeTag(r, order)
if err != nil {
return nil, 0, err
}
d.Tags = append(d.Tags, t)
}
// get offset to next ifd
err = binary.Read(r, order, &offset)
if err != nil {
return nil, 0, errors.New("tiff: falied to read offset to next IFD: " + err.Error())
}
return d, offset, nil
}
func (d *Dir) String() string {
s := "Dir{"
for _, t := range d.Tags {
s += t.String() + ", "
}
return s + "}"
}

235
Godeps/_workspace/src/github.com/rwcarlsen/goexif/tiff/tiff_test.go сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,235 @@
package tiff
import (
"bytes"
"encoding/binary"
"encoding/hex"
"flag"
"os"
"path/filepath"
"testing"
)
var dataDir = flag.String("test_data_dir", ".", "Directory where the data files for testing are located")
type input struct {
tgId string
tpe string
nVals string
offset string
val string
}
type output struct {
id uint16
typ DataType
count uint32
val []byte
}
type tagTest struct {
big input // big endian
little input // little endian
out output
}
///////////////////////////////////////////////
//// Big endian Tests /////////////////////////
///////////////////////////////////////////////
var set1 = []tagTest{
//////////// string type //////////////
tagTest{
// {"TgId", "TYPE", "N-VALUES", "OFFSET--", "VAL..."},
input{"0003", "0002", "00000002", "11000000", ""},
input{"0300", "0200", "02000000", "11000000", ""},
output{0x0003, DataType(0x0002), 0x0002, []byte{0x11, 0x00}},
},
tagTest{
input{"0001", "0002", "00000006", "00000012", "111213141516"},
input{"0100", "0200", "06000000", "12000000", "111213141516"},
output{0x0001, DataType(0x0002), 0x0006, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}},
},
//////////// int (1-byte) type ////////////////
tagTest{
input{"0001", "0001", "00000001", "11000000", ""},
input{"0100", "0100", "01000000", "11000000", ""},
output{0x0001, DataType(0x0001), 0x0001, []byte{0x11}},
},
tagTest{
input{"0001", "0001", "00000005", "00000010", "1112131415"},
input{"0100", "0100", "05000000", "10000000", "1112131415"},
output{0x0001, DataType(0x0001), 0x0005, []byte{0x11, 0x12, 0x13, 0x14, 0x15}},
},
tagTest{
input{"0001", "0006", "00000001", "11000000", ""},
input{"0100", "0600", "01000000", "11000000", ""},
output{0x0001, DataType(0x0006), 0x0001, []byte{0x11}},
},
tagTest{
input{"0001", "0006", "00000005", "00000010", "1112131415"},
input{"0100", "0600", "05000000", "10000000", "1112131415"},
output{0x0001, DataType(0x0006), 0x0005, []byte{0x11, 0x12, 0x13, 0x14, 0x15}},
},
//////////// int (2-byte) types ////////////////
tagTest{
input{"0001", "0003", "00000002", "11111212", ""},
input{"0100", "0300", "02000000", "11111212", ""},
output{0x0001, DataType(0x0003), 0x0002, []byte{0x11, 0x11, 0x12, 0x12}},
},
tagTest{
input{"0001", "0003", "00000003", "00000010", "111213141516"},
input{"0100", "0300", "03000000", "10000000", "111213141516"},
output{0x0001, DataType(0x0003), 0x0003, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}},
},
tagTest{
input{"0001", "0008", "00000001", "11120000", ""},
input{"0100", "0800", "01000000", "11120000", ""},
output{0x0001, DataType(0x0008), 0x0001, []byte{0x11, 0x12}},
},
tagTest{
input{"0001", "0008", "00000003", "00000100", "111213141516"},
input{"0100", "0800", "03000000", "00100000", "111213141516"},
output{0x0001, DataType(0x0008), 0x0003, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16}},
},
//////////// int (4-byte) types ////////////////
tagTest{
input{"0001", "0004", "00000001", "11121314", ""},
input{"0100", "0400", "01000000", "11121314", ""},
output{0x0001, DataType(0x0004), 0x0001, []byte{0x11, 0x12, 0x13, 0x14}},
},
tagTest{
input{"0001", "0004", "00000002", "00000010", "1112131415161718"},
input{"0100", "0400", "02000000", "10000000", "1112131415161718"},
output{0x0001, DataType(0x0004), 0x0002, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}},
},
tagTest{
input{"0001", "0009", "00000001", "11121314", ""},
input{"0100", "0900", "01000000", "11121314", ""},
output{0x0001, DataType(0x0009), 0x0001, []byte{0x11, 0x12, 0x13, 0x14}},
},
tagTest{
input{"0001", "0009", "00000002", "00000011", "1112131415161819"},
input{"0100", "0900", "02000000", "11000000", "1112131415161819"},
output{0x0001, DataType(0x0009), 0x0002, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}},
},
//////////// rational types ////////////////////
tagTest{
input{"0001", "0005", "00000001", "00000010", "1112131415161718"},
input{"0100", "0500", "01000000", "10000000", "1112131415161718"},
output{0x0001, DataType(0x0005), 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}},
},
tagTest{
input{"0001", "000A", "00000001", "00000011", "1112131415161819"},
input{"0100", "0A00", "01000000", "11000000", "1112131415161819"},
output{0x0001, DataType(0x000A), 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}},
},
//////////// float types ///////////////////////
tagTest{
input{"0001", "0005", "00000001", "00000010", "1112131415161718"},
input{"0100", "0500", "01000000", "10000000", "1112131415161718"},
output{0x0001, DataType(0x0005), 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18}},
},
tagTest{
input{"0101", "000A", "00000001", "00000011", "1112131415161819"},
input{"0101", "0A00", "01000000", "11000000", "1112131415161819"},
output{0x0101, DataType(0x000A), 0x0001, []byte{0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19}},
},
}
func TestDecodeTag(t *testing.T) {
for i, tst := range set1 {
testSingle(t, binary.BigEndian, tst.big, tst.out, i)
testSingle(t, binary.LittleEndian, tst.little, tst.out, i)
}
}
func testSingle(t *testing.T, order binary.ByteOrder, in input, out output, i int) {
data := buildInput(in, order)
buf := bytes.NewReader(data)
tg, err := DecodeTag(buf, order)
if err != nil {
t.Errorf("(%v) tag %v%+v decode failed: %v", order, i, in, err)
return
}
if tg.Id != out.id {
t.Errorf("(%v) tag %v id decode: expected %v, got %v", order, i, out.id, tg.Id)
}
if tg.Type != out.typ {
t.Errorf("(%v) tag %v type decode: expected %v, got %v", order, i, out.typ, tg.Type)
}
if tg.Count != out.count {
t.Errorf("(%v) tag %v component count decode: expected %v, got %v", order, i, out.count, tg.Count)
}
if !bytes.Equal(tg.Val, out.val) {
t.Errorf("(%v) tag %v value decode: expected %v, got %v", order, i, out.val, tg.Val)
}
}
// buildInputBig creates a byte-slice based on big-endian ordered input
func buildInput(in input, order binary.ByteOrder) []byte {
data := make([]byte, 0)
d, _ := hex.DecodeString(in.tgId)
data = append(data, d...)
d, _ = hex.DecodeString(in.tpe)
data = append(data, d...)
d, _ = hex.DecodeString(in.nVals)
data = append(data, d...)
d, _ = hex.DecodeString(in.offset)
data = append(data, d...)
if in.val != "" {
off := order.Uint32(d)
for i := 0; i < int(off)-12; i++ {
data = append(data, 0xFF)
}
d, _ = hex.DecodeString(in.val)
data = append(data, d...)
}
return data
}
func TestDecode(t *testing.T) {
name := filepath.Join(*dataDir, "sample1.tif")
f, err := os.Open(name)
if err != nil {
t.Fatalf("%v\n", err)
}
tif, err := Decode(f)
if err != nil {
t.Fatal(err)
}
t.Log(tif)
}
func TestDecodeTag_blob(t *testing.T) {
buf := bytes.NewReader(data())
buf.Seek(10, 1)
tg, err := DecodeTag(buf, binary.LittleEndian)
if err != nil {
t.Fatalf("tag decode failed: %v", err)
}
t.Logf("tag: %v+\n", tg)
n, d, err := tg.Rat2(0)
if err != nil {
t.Fatalf("tag decoded wrong type: %v", err)
}
t.Logf("tag rat val: %v/%v\n", n, d)
}
func data() []byte {
s1 := "49492A000800000002001A0105000100"
s1 += "00002600000069870400010000001102"
s1 += "0000000000004800000001000000"
dat, err := hex.DecodeString(s1)
if err != nil {
panic("invalid string fixture")
}
return dat
}

Просмотреть файл

@@ -3,6 +3,8 @@
GOPATH ?= $(GOPATH:)
GOFLAGS ?= $(GOFLAGS:)
BUILD_NUMBER ?= $(BUILD_NUMBER:)
BUILD_DATE = $(shell date -u)
BUILD_HASH = $(shell git rev-parse HEAD)
GO=$(GOPATH)/bin/godep go
ESLINT=node_modules/eslint/bin/eslint.js
@@ -32,6 +34,11 @@ all: travis
travis:
@echo building for travis
if [ "$(TRAVIS_DB)" = "postgres" ]; then \
sed -i'.bak' 's|mysql|postgres|g' config/config.json; \
sed -i'.bak' 's|mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8|postgres://mmuser:mostest@dockerhost:5432/mattermost_test?sslmode=disable\&connect_timeout=10|g' config/config.json; \
fi
rm -Rf $(DIST_ROOT)
@$(GO) clean $(GOFLAGS) -i ./...
@@ -49,6 +56,10 @@ travis:
exit 1; \
fi
@sed -i'.bak' 's|_BUILD_NUMBER_|$(BUILD_NUMBER)|g' ./model/version.go
@sed -i'.bak' 's|_BUILD_DATE_|$(BUILD_DATE)|g' ./model/version.go
@sed -i'.bak' 's|_BUILD_HASH_|$(BUILD_HASH)|g' ./model/version.go
@$(GO) build $(GOFLAGS) ./...
@$(GO) install $(GOFLAGS) ./...
@@ -222,6 +233,10 @@ cleandb:
fi
dist: install
@sed -i'.bak' 's|_BUILD_NUMBER_|$(BUILD_NUMBER)|g' ./model/version.go
@sed -i'.bak' 's|_BUILD_DATE_|$(BUILD_DATE)|g' ./model/version.go
@sed -i'.bak' 's|_BUILD_HASH_|$(BUILD_HASH)|g' ./model/version.go
@$(GO) build $(GOFLAGS) -i ./...
@$(GO) install $(GOFLAGS) ./...

Просмотреть файл

@@ -48,8 +48,10 @@ There are multiple ways to install Mattermost depending on your needs.
#### Development Install
- [Developer Machine Setup](doc/install/dev-setup.md) - Setup your local machine development environment using Docker on Mac OSX or Ubuntu.
- [Developer Machine Setup](doc/install/dev-setup.md) - Setup your local machine development environment using Docker on Mac OSX or Ubuntu. Pull the latest stable release or pull the latest code from our development build.
[![Build Status](https://travis-ci.org/mattermost/platform.svg?branch=master)](https://travis-ci.org/mattermost/platform)
#### Production Deployment (for Beta2 and later)
Prior to production installation, please review [Mattermost system requirements](doc/install/requirements.md).

Просмотреть файл

@@ -7,6 +7,7 @@ import (
"bufio"
"net/http"
"os"
"strings"
l4g "code.google.com/p/log4go"
"github.com/gorilla/mux"
@@ -20,6 +21,8 @@ func InitAdmin(r *mux.Router) {
sr := r.PathPrefix("/admin").Subrouter()
sr.Handle("/logs", ApiUserRequired(getLogs)).Methods("GET")
sr.Handle("/config", ApiUserRequired(getConfig)).Methods("GET")
sr.Handle("/save_config", ApiUserRequired(saveConfig)).Methods("POST")
sr.Handle("/client_props", ApiAppHandler(getClientProperties)).Methods("GET")
}
@@ -33,7 +36,7 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
if utils.Cfg.LogSettings.FileEnable {
file, err := os.Open(utils.Cfg.LogSettings.FileLocation)
file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation))
if err != nil {
c.Err = model.NewAppError("getLogs", "Error reading log file", err.Error())
}
@@ -54,3 +57,44 @@ func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
func getClientProperties(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(utils.ClientProperties)))
}
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getConfig") {
return
}
json := utils.Cfg.ToJson()
cfg := model.ConfigFromJson(strings.NewReader(json))
json = cfg.ToJson()
w.Write([]byte(json))
}
func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.HasSystemAdminPermissions("getConfig") {
return
}
cfg := model.ConfigFromJson(r.Body)
if cfg == nil {
c.SetInvalidParam("saveConfig", "config")
return
}
if len(cfg.ServiceSettings.Port) == 0 {
c.SetInvalidParam("saveConfig", "config")
return
}
if cfg.TeamSettings.MaxUsersPerTeam == 0 {
c.SetInvalidParam("saveConfig", "config")
return
}
// TODO run some cleanup validators
utils.SaveConfig(utils.CfgFileName, cfg)
utils.LoadConfig(utils.CfgFileName)
json := utils.Cfg.ToJson()
w.Write([]byte(json))
}

Просмотреть файл

@@ -8,6 +8,7 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
func TestGetLogs(t *testing.T) {
@@ -20,6 +21,12 @@ func TestGetLogs(t *testing.T) {
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.GetLogs(); err == nil {
t.Fatal("Shouldn't have permissions")
}
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
@@ -37,8 +44,81 @@ func TestGetLogs(t *testing.T) {
func TestGetClientProperties(t *testing.T) {
Setup()
if _, err := Client.GetClientProperties(); err != nil {
if result, err := Client.GetClientProperties(); err != nil {
t.Fatal(err)
} else {
props := result.Data.(map[string]string)
if len(props["Version"]) == 0 {
t.Fatal()
}
}
}
func TestGetConfig(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.GetConfig(); err == nil {
t.Fatal("Shouldn't have permissions")
}
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
if result, err := Client.GetConfig(); err != nil {
t.Fatal(err)
} else {
cfg := result.Data.(*model.Config)
if len(cfg.ServiceSettings.SiteName) == 0 {
t.Fatal()
}
}
}
func TestSaveConfig(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
Client.LoginByEmail(team.Name, user.Email, "pwd")
if _, err := Client.SaveConfig(utils.Cfg); err == nil {
t.Fatal("Shouldn't have permissions")
}
c := &Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
Client.LoginByEmail(team.Name, user.Email, "pwd")
if result, err := Client.SaveConfig(utils.Cfg); err != nil {
t.Fatal(err)
} else {
cfg := result.Data.(*model.Config)
if len(cfg.ServiceSettings.SiteName) == 0 {
t.Fatal()
}
}
}

Просмотреть файл

@@ -57,7 +57,7 @@ func TestCreateChannel(t *testing.T) {
rchannel.Data.(*model.Channel).Id = ""
if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err != nil {
if err.Message != "A channel with that handle already exists" {
if err.Message != "A channel with that URL already exists" {
t.Fatal(err)
}
}
@@ -68,7 +68,7 @@ func TestCreateChannel(t *testing.T) {
Client.DeleteChannel(savedId)
if _, err := Client.CreateChannel(rchannel.Data.(*model.Channel)); err != nil {
if err.Message != "A channel with that handle was previously created" {
if err.Message != "A channel with that URL was previously created" {
t.Fatal(err)
}
}

Просмотреть файл

@@ -341,7 +341,7 @@ func loadTestSetupCommand(c *Context, command *model.Command) bool {
}
}
} else {
client.MockSession(c.Session.Id)
client.MockSession(c.Session.Token)
CreateTestEnviromentInTeam(
client,
c.Session.TeamId,
@@ -406,7 +406,7 @@ func loadTestChannelsCommand(c *Context, command *model.Command) bool {
channelsr = utils.Range{20, 30}
}
client := model.NewClient(c.GetSiteURL())
client.MockSession(c.Session.Id)
client.MockSession(c.Session.Token)
channelCreator := NewAutoChannelCreator(client, c.Session.TeamId)
channelCreator.Fuzzy = doFuzz
channelCreator.CreateTestChannels(channelsr)
@@ -458,7 +458,7 @@ func loadTestPostsCommand(c *Context, command *model.Command) bool {
}
client := model.NewClient(c.GetSiteURL())
client.MockSession(c.Session.Id)
client.MockSession(c.Session.Token)
testPoster := NewAutoPostCreator(client, command.ChannelId)
testPoster.Fuzzy = doFuzz
testPoster.Users = usernames

Просмотреть файл

@@ -125,7 +125,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.setSiteURL(protocol + "://" + r.Host)
w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
w.Header().Set(model.HEADER_VERSION_ID, utils.Cfg.ServiceSettings.Version+fmt.Sprintf(".%v", utils.CfgLastModified))
w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v", model.CurrentVersion, utils.CfgLastModified))
// Instruct the browser not to display us in an iframe for anti-clickjacking
if !h.isApi {

Просмотреть файл

@@ -5,6 +5,7 @@ package api
import (
"bytes"
"code.google.com/p/graphics-go/graphics"
l4g "code.google.com/p/log4go"
"fmt"
"github.com/goamz/goamz/aws"
@@ -13,6 +14,7 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/nfnt/resize"
"github.com/rwcarlsen/goexif/exif"
_ "golang.org/x/image/bmp"
"image"
"image/color"
@@ -21,6 +23,7 @@ import (
"image/jpeg"
"io"
"io/ioutil"
"math"
"mime"
"net/http"
"net/url"
@@ -31,6 +34,27 @@ import (
"time"
)
const (
/*
EXIF Image Orientations
1 2 3 4 5 6 7 8
888888 888888 88 88 8888888888 88 88 8888888888
88 88 88 88 88 88 88 88 88 88 88 88
8888 8888 8888 8888 88 8888888888 8888888888 88
88 88 88 88
88 88 888888 888888
*/
Upright = 1
UprightMirrored = 2
UpsideDown = 3
UpsideDownMirrored = 4
RotatedCWMirrored = 5
RotatedCCW = 6
RotatedCCWMirrored = 7
RotatedCW = 8
)
var fileInfoCache *utils.Cache = utils.NewLru(1000)
func InitFile(r *mux.Router) {
@@ -144,25 +168,59 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
return
}
// Decode image config
imgConfig, _, err := image.DecodeConfig(bytes.NewReader(fileData[i]))
if err != nil {
l4g.Error("Unable to decode image config channelId=%v userId=%v filename=%v err=%v", channelId, userId, filename, err)
return
width := img.Bounds().Dx()
height := img.Bounds().Dy()
// Get the image's orientation and ignore any errors since not all images will have orientation data
orientation, _ := getImageOrientation(fileData[i])
// Create a temporary image that will be manipulated and then used to make the thumbnail and preview image
var temp *image.RGBA
switch orientation {
case Upright, UprightMirrored, UpsideDown, UpsideDownMirrored:
temp = image.NewRGBA(img.Bounds())
case RotatedCCW, RotatedCCWMirrored, RotatedCW, RotatedCWMirrored:
bounds := img.Bounds()
temp = image.NewRGBA(image.Rect(bounds.Min.Y, bounds.Min.X, bounds.Max.Y, bounds.Max.X))
width, height = height, width
}
// Remove transparency due to JPEG's lack of support of it
temp := image.NewRGBA(img.Bounds())
// Draw a white background since JPEGs lack transparency
draw.Draw(temp, temp.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
draw.Draw(temp, temp.Bounds(), img, img.Bounds().Min, draw.Over)
// Copy the original image onto the temporary one while rotating it as necessary
switch orientation {
case UpsideDown, UpsideDownMirrored:
// rotate 180 degrees
err := graphics.Rotate(temp, img, &graphics.RotateOptions{Angle: math.Pi})
if err != nil {
l4g.Error("Unable to rotate image")
}
case RotatedCW, RotatedCWMirrored:
// rotate 90 degrees CCW
graphics.Rotate(temp, img, &graphics.RotateOptions{Angle: 3 * math.Pi / 2})
if err != nil {
l4g.Error("Unable to rotate image")
}
case RotatedCCW, RotatedCCWMirrored:
// rotate 90 degrees CW
graphics.Rotate(temp, img, &graphics.RotateOptions{Angle: math.Pi / 2})
if err != nil {
l4g.Error("Unable to rotate image")
}
case Upright, UprightMirrored:
draw.Draw(temp, temp.Bounds(), img, img.Bounds().Min, draw.Over)
}
img = temp
// Create thumbnail
go func() {
thumbWidth := float64(utils.Cfg.ImageSettings.ThumbnailWidth)
thumbHeight := float64(utils.Cfg.ImageSettings.ThumbnailHeight)
imgWidth := float64(imgConfig.Width)
imgHeight := float64(imgConfig.Height)
imgWidth := float64(width)
imgHeight := float64(height)
var thumbnail image.Image
if imgHeight < thumbHeight && imgWidth < thumbWidth {
@@ -189,7 +247,7 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
// Create preview
go func() {
var preview image.Image
if imgConfig.Width > int(utils.Cfg.ImageSettings.PreviewWidth) {
if width > int(utils.Cfg.ImageSettings.PreviewWidth) {
preview = resize.Resize(utils.Cfg.ImageSettings.PreviewWidth, utils.Cfg.ImageSettings.PreviewHeight, img, resize.Lanczos3)
} else {
preview = img
@@ -213,6 +271,23 @@ func fireAndForgetHandleImages(filenames []string, fileData [][]byte, teamId, ch
}()
}
func getImageOrientation(imageData []byte) (int, error) {
if exifData, err := exif.Decode(bytes.NewReader(imageData)); err != nil {
return Upright, err
} else {
if tag, err := exifData.Get("Orientation"); err != nil {
return Upright, err
} else {
orientation, err := tag.Int(0)
if err != nil {
return Upright, err
} else {
return orientation, nil
}
}
}
}
type ImageGetResult struct {
Error error
ImageData []byte

Просмотреть файл

@@ -9,13 +9,11 @@
},
"ServiceSettings": {
"SiteName": "Mattermost",
"Mode" : "dev",
"AllowTesting" : false,
"Mode": "dev",
"AllowTesting": false,
"UseSSL": false,
"Port": "8065",
"Version": "developer",
"Shards": {
},
"InviteSalt": "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6",
"PublicLinkSalt": "TO3pTyXIZzwHiwyZgGql7lM7DG3zeId4",
"ResetSalt": "IPxFzSfnDFsNsRafZxz8NaYqFKhf9y2t",
@@ -26,21 +24,12 @@
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false
},
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret" : "",
"Id": "",
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
},
"SqlSettings": {
"DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8",
"DataSourceReplicas": ["mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"],
"DataSourceReplicas": [
"mmuser:mostest@tcp(dockerhost:3306)/mattermost_test?charset=utf8mb4,utf8"
],
"MaxIdleConns": 10,
"MaxOpenConns": 10,
"Trace": false,
@@ -62,7 +51,7 @@
"InitialFont": "luximbi.ttf"
},
"EmailSettings": {
"ByPassEmail" : true,
"ByPassEmail": true,
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
@@ -95,8 +84,20 @@
"MaxUsersPerTeam": 150,
"AllowPublicLink": true,
"AllowValetDefault": false,
"TourLink": "",
"DefaultThemeColor": "#2389D7",
"DisableTeamCreation": false,
"RestrictCreationToDomains": ""
},
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret": "",
"Id": "",
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
}
}
}

Просмотреть файл

@@ -31,7 +31,26 @@ git checkout -b <branch name>
## Submitting a Pull Request
1. Please add yourself to the contributor list prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/).
1. Please add yourself to the Mattermost [approved contributor list](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true) prior to submitting by completing the [contributor license agreement](http://www.mattermost.org/mattermost-contributor-agreement/).
For pull requests made by contributors not yet added to the approved contributor list, a reviewer may respond:
```
Thanks @[GITHUB_USERNAME] for the pull request!
Before we can review, we need to add you to the list of approved contributors for the Mattermost project.
**Please help complete the Mattermost [contribution license agreement](http://www.mattermost.org/mattermost-contributor-agreement/)?**
This is a standard procedure for many open source projects. You can view a list of past contributors who have completed the form [here](https://docs.google.com/spreadsheets/d/1NTCeG-iL_VS9bFqtmHSfwETo5f-8MQ7oMDE5IUYJi_Y/pubhtml?gid=0&single=true).
After completing the form, it should be processed within 24 hours and reviewers for your pull request will be able to proceed.
Please let us know if you have any questions.
We are very happy to have you join our growing community!
```
2. When you submit your pull request please include the Ticket ID at the beginning of your pull request comment, followed by a colon.

Просмотреть файл

@@ -10,7 +10,7 @@
"ServiceSettings": {
"SiteName": "Mattermost",
"Mode" : "dev",
"AllowTesting" : false,
"AllowTesting" : false,
"UseSSL": false,
"Port": "80",
"Version": "developer",
@@ -63,8 +63,8 @@
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
"UseTLS": false,
"UseStartTLS": false,
"UseTLS": false,
"UseStartTLS": false,
"FeedbackEmail": "",
"FeedbackName": "",
"ApplePushServer": "",

Просмотреть файл

@@ -10,7 +10,7 @@
"ServiceSettings": {
"SiteName": "Mattermost",
"Mode" : "dev",
"AllowTesting" : true,
"AllowTesting" : true,
"UseSSL": false,
"Port": "80",
"Version": "developer",
@@ -65,8 +65,8 @@
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
"UseTLS": false,
"UseStartTLS": false,
"UseTLS": false,
"UseStartTLS": false,
"FeedbackEmail": "",
"FeedbackName": "",
"ApplePushServer": "",

Просмотреть файл

@@ -10,7 +10,7 @@
"ServiceSettings": {
"SiteName": "Mattermost",
"Mode" : "dev",
"AllowTesting" : true,
"AllowTesting" : true,
"UseSSL": false,
"Port": "80",
"Version": "developer",
@@ -66,8 +66,8 @@
"SMTPUsername": "",
"SMTPPassword": "",
"SMTPServer": "",
"UseTLS": false,
"UseStartTLS": false,
"UseTLS": false,
"UseStartTLS": false,
"FeedbackEmail": "",
"FeedbackName": "",
"ApplePushServer": "",

Просмотреть файл

@@ -23,6 +23,7 @@ import (
var flagCmdCreateTeam bool
var flagCmdCreateUser bool
var flagCmdAssignRole bool
var flagCmdVersion bool
var flagCmdResetPassword bool
var flagConfigFile string
var flagEmail string
@@ -42,6 +43,7 @@ func main() {
}
pwd, _ := os.Getwd()
l4g.Info("Current version is %v (%v/%v/%v)", model.CurrentVersion, model.BuildNumber, model.BuildDate, model.BuildHash)
l4g.Info("Current working directory is %v", pwd)
l4g.Info("Loaded config file from %v", utils.FindConfigFile(flagConfigFile))
@@ -83,14 +85,16 @@ func parseCmds() {
flag.BoolVar(&flagCmdCreateTeam, "create_team", false, "")
flag.BoolVar(&flagCmdCreateUser, "create_user", false, "")
flag.BoolVar(&flagCmdAssignRole, "assign_role", false, "")
flag.BoolVar(&flagCmdVersion, "version", false, "")
flag.BoolVar(&flagCmdResetPassword, "reset_password", false, "")
flag.Parse()
flagRunCmds = flagCmdCreateTeam || flagCmdCreateUser || flagCmdAssignRole || flagCmdResetPassword
flagRunCmds = flagCmdCreateTeam || flagCmdCreateUser || flagCmdAssignRole || flagCmdResetPassword || flagCmdVersion
}
func runCmds() {
cmdVersion()
cmdCreateTeam()
cmdCreateUser()
cmdAssignRole()
@@ -184,6 +188,17 @@ func cmdCreateUser() {
}
}
func cmdVersion() {
if flagCmdVersion {
fmt.Fprintln(os.Stderr, "Version: "+model.CurrentVersion)
fmt.Fprintln(os.Stderr, "Build Number: "+model.BuildNumber)
fmt.Fprintln(os.Stderr, "Build Date: "+model.BuildDate)
fmt.Fprintln(os.Stderr, "Build Hash: "+model.BuildHash)
os.Exit(0)
}
}
func cmdAssignRole() {
if flagCmdAssignRole {
if len(flagTeamName) == 0 {
@@ -298,6 +313,8 @@ Usage:
platform [options]
-version Display the current version
-config="config.json" Path to the config file
-email="user@example.com" Email address used in other commands

Просмотреть файл

@@ -385,6 +385,24 @@ func (c *Client) GetClientProperties() (*Result, *AppError) {
}
}
func (c *Client) GetConfig() (*Result, *AppError) {
if r, err := c.DoApiGet("/admin/config", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil
}
}
func (c *Client) SaveConfig(config *Config) (*Result, *AppError) {
if r, err := c.DoApiPost("/admin/save_config", config.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ConfigFromJson(r.Body)}, nil
}
}
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err
@@ -790,4 +808,5 @@ func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) {
func (c *Client) MockSession(sessionToken string) {
c.AuthToken = sessionToken
c.AuthType = HEADER_BEARER
}

151
model/config.go Обычный файл
Просмотреть файл

@@ -0,0 +1,151 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type ServiceSettings struct {
SiteName string
Mode string
AllowTesting bool
UseSSL bool
Port string
Version string
InviteSalt string
PublicLinkSalt string
ResetSalt string
AnalyticsUrl string
UseLocalStorage bool
StorageDirectory string
AllowedLoginAttempts int
DisableEmailSignUp bool
EnableOAuthServiceProvider bool
}
type SSOSetting struct {
Allow bool
Secret string
Id string
Scope string
AuthEndpoint string
TokenEndpoint string
UserApiEndpoint string
}
type SqlSettings struct {
DriverName string
DataSource string
DataSourceReplicas []string
MaxIdleConns int
MaxOpenConns int
Trace bool
AtRestEncryptKey string
}
type LogSettings struct {
ConsoleEnable bool
ConsoleLevel string
FileEnable bool
FileLevel string
FileFormat string
FileLocation string
}
type AWSSettings struct {
S3AccessKeyId string
S3SecretAccessKey string
S3Bucket string
S3Region string
}
type ImageSettings struct {
ThumbnailWidth uint
ThumbnailHeight uint
PreviewWidth uint
PreviewHeight uint
ProfileWidth uint
ProfileHeight uint
InitialFont string
}
type EmailSettings struct {
ByPassEmail bool
SMTPUsername string
SMTPPassword string
SMTPServer string
UseTLS bool
UseStartTLS bool
FeedbackEmail string
FeedbackName string
ApplePushServer string
ApplePushCertPublic string
ApplePushCertPrivate string
}
type RateLimitSettings struct {
UseRateLimiter bool
PerSec int
MemoryStoreSize int
VaryByRemoteAddr bool
VaryByHeader string
}
type PrivacySettings struct {
ShowEmailAddress bool
ShowPhoneNumber bool
ShowSkypeId bool
ShowFullName bool
}
type ClientSettings struct {
SegmentDeveloperKey string
GoogleDeveloperKey string
}
type TeamSettings struct {
MaxUsersPerTeam int
AllowPublicLink bool
AllowValetDefault bool
TourLink string
DefaultThemeColor string
DisableTeamCreation bool
RestrictCreationToDomains string
}
type Config struct {
LogSettings LogSettings
ServiceSettings ServiceSettings
SqlSettings SqlSettings
AWSSettings AWSSettings
ImageSettings ImageSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
ClientSettings ClientSettings
TeamSettings TeamSettings
SSOSettings map[string]SSOSetting
}
func (o *Config) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func ConfigFromJson(data io.Reader) *Config {
decoder := json.NewDecoder(data)
var o Config
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

34
model/system.go Обычный файл
Просмотреть файл

@@ -0,0 +1,34 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type System struct {
Name string `json:"name"`
Value string `json:"value"`
}
func (o *System) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
func SystemFromJson(data io.Reader) *System {
decoder := json.NewDecoder(data)
var o System
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

19
model/system_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
"testing"
)
func TestSystemJson(t *testing.T) {
system := System{Name: "test", Value: NewId()}
json := system.ToJson()
result := SystemFromJson(strings.NewReader(json))
if result.Name != "test" {
t.Fatal("Ids do not match")
}
}

Просмотреть файл

@@ -16,11 +16,6 @@ import (
"time"
)
const (
// Also change web/react/stores/browser_store.jsx BROWSER_STORE_VERSION
ETAG_ROOT_VERSION = "12"
)
type StringMap map[string]string
type StringArray []string
type EncryptStringMap map[string]string
@@ -235,7 +230,7 @@ func IsValidAlphaNum(s string, allowUnderscores bool) bool {
func Etag(parts ...interface{}) string {
etag := ETAG_ROOT_VERSION
etag := CurrentVersion
for _, part := range parts {
etag += fmt.Sprintf(".%v", part)

90
model/version.go Обычный файл
Просмотреть файл

@@ -0,0 +1,90 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strconv"
"strings"
)
// This is a list of all the current viersions including any patches.
// It should be maitained in chronological order with most current
// release at the front of the list.
var versions = []string{
"0.8.0",
"0.7.1",
"0.7.0",
"0.6.0",
"0.5.0",
}
var CurrentVersion string = versions[0]
var BuildNumber = "_BUILD_NUMBER_"
var BuildDate = "_BUILD_DATE_"
var BuildHash = "_BUILD_HASH_"
func SplitVersion(version string) (int64, int64, int64) {
parts := strings.Split(version, ".")
major := int64(0)
minor := int64(0)
patch := int64(0)
if len(parts) > 0 {
major, _ = strconv.ParseInt(parts[0], 10, 64)
}
if len(parts) > 1 {
minor, _ = strconv.ParseInt(parts[1], 10, 64)
}
if len(parts) > 2 {
patch, _ = strconv.ParseInt(parts[2], 10, 64)
}
return major, minor, patch
}
func GetPreviousVersion(currentVersion string) (int64, int64) {
currentIndex := -1
currentMajor, currentMinor, _ := SplitVersion(currentVersion)
for index, version := range versions {
major, minor, _ := SplitVersion(version)
if currentMajor == major && currentMinor == minor {
currentIndex = index
}
if currentIndex >= 0 {
if currentMajor != major || currentMinor != minor {
return major, minor
}
}
}
return 0, 0
}
func IsCurrentVersion(versionToCheck string) bool {
currentMajor, currentMinor, _ := SplitVersion(CurrentVersion)
toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck)
if toCheckMajor == currentMajor && toCheckMinor == currentMinor {
return true
} else {
return false
}
}
func IsPreviousVersion(versionToCheck string) bool {
toCheckMajor, toCheckMinor, _ := SplitVersion(versionToCheck)
prevMajor, prevMinor := GetPreviousVersion(CurrentVersion)
if toCheckMajor == prevMajor && toCheckMinor == prevMinor {
return true
} else {
return false
}
}

74
model/version_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,74 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"fmt"
"testing"
)
func TestSplitVersion(t *testing.T) {
major1, minor1, patch1 := SplitVersion("junk")
if major1 != 0 || minor1 != 0 || patch1 != 0 {
t.Fatal()
}
major2, minor2, patch2 := SplitVersion("1.2.3")
if major2 != 1 || minor2 != 2 || patch2 != 3 {
t.Fatal()
}
major3, minor3, patch3 := SplitVersion("1.2")
if major3 != 1 || minor3 != 2 || patch3 != 0 {
t.Fatal()
}
major4, minor4, patch4 := SplitVersion("1")
if major4 != 1 || minor4 != 0 || patch4 != 0 {
t.Fatal()
}
major5, minor5, patch5 := SplitVersion("1.2.3.junkgoeswhere")
if major5 != 1 || minor5 != 2 || patch5 != 3 {
t.Fatal()
}
}
func TestGetPreviousVersion(t *testing.T) {
if major, minor := GetPreviousVersion("0.8.0"); major != 0 || minor != 7 {
t.Fatal(major, minor)
}
if major, minor := GetPreviousVersion("0.7.0"); major != 0 || minor != 6 {
t.Fatal(major, minor)
}
if major, minor := GetPreviousVersion("0.7.1"); major != 0 || minor != 6 {
t.Fatal(major, minor)
}
if major, minor := GetPreviousVersion("0.7111.1"); major != 0 || minor != 0 {
t.Fatal(major, minor)
}
}
func TestIsCurrentVersion(t *testing.T) {
major, minor, patch := SplitVersion(CurrentVersion)
if !IsCurrentVersion(CurrentVersion) {
t.Fatal()
}
if !IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major, minor, patch+100)) {
t.Fatal()
}
if IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major, minor+1, patch)) {
t.Fatal()
}
if IsCurrentVersion(fmt.Sprintf("%v.%v.%v", major+1, minor, patch)) {
t.Fatal()
}
}

Просмотреть файл

@@ -37,7 +37,6 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
}
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
s.CreateColumnIfNotExists("Channels", "CreatorId", "varchar(26)", "character varying(26)", "")
}
func (s SqlChannelStore) CreateIndexesIfNotExists() {
@@ -86,9 +85,9 @@ func (s SqlChannelStore) Save(channel *model.Channel) StoreChannel {
dupChannel := model.Channel{}
s.GetReplica().SelectOne(&dupChannel, "SELECT * FROM Channels WHERE TeamId = :TeamId AND Name = :Name AND DeleteAt > 0", map[string]interface{}{"TeamId": channel.TeamId, "Name": channel.Name})
if dupChannel.DeleteAt > 0 {
result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that handle was previously created", "id="+channel.Id+", "+err.Error())
result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL was previously created", "id="+channel.Id+", "+err.Error())
} else {
result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that handle already exists", "id="+channel.Id+", "+err.Error())
result.Err = model.NewAppError("SqlChannelStore.Update", "A channel with that URL already exists", "id="+channel.Id+", "+err.Error())
}
} else {
result.Err = model.NewAppError("SqlChannelStore.Save", "We couldn't save the channel", "id="+channel.Id+", "+err.Error())

Просмотреть файл

@@ -196,9 +196,9 @@ func (s SqlPostStore) GetEtag(channelId string) StoreChannel {
var et etagPosts
err := s.GetReplica().SelectOne(&et, "SELECT Id, UpdateAt FROM Posts WHERE ChannelId = :ChannelId ORDER BY UpdateAt DESC LIMIT 1", map[string]interface{}{"ChannelId": channelId})
if err != nil {
result.Data = fmt.Sprintf("%v.0.%v", model.ETAG_ROOT_VERSION, model.GetMillis())
result.Data = fmt.Sprintf("%v.0.%v", model.CurrentVersion, model.GetMillis())
} else {
result.Data = fmt.Sprintf("%v.%v.%v", model.ETAG_ROOT_VERSION, et.Id, et.UpdateAt)
result.Data = fmt.Sprintf("%v.%v.%v", model.CurrentVersion, et.Id, et.UpdateAt)
}
storeChannel <- result

Просмотреть файл

@@ -37,14 +37,14 @@ func TestPostStoreGet(t *testing.T) {
o1.Message = "a" + model.NewId() + "b"
etag1 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string)
if strings.Index(etag1, model.ETAG_ROOT_VERSION+".0.") != 0 {
if strings.Index(etag1, model.CurrentVersion+".0.") != 0 {
t.Fatal("Invalid Etag")
}
o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
etag2 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string)
if strings.Index(etag2, model.ETAG_ROOT_VERSION+"."+o1.Id) != 0 {
if strings.Index(etag2, model.CurrentVersion+"."+o1.Id) != 0 {
t.Fatal("Invalid Etag")
}
@@ -136,7 +136,7 @@ func TestPostStoreDelete(t *testing.T) {
o1.Message = "a" + model.NewId() + "b"
etag1 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string)
if strings.Index(etag1, model.ETAG_ROOT_VERSION+".0.") != 0 {
if strings.Index(etag1, model.CurrentVersion+".0.") != 0 {
t.Fatal("Invalid Etag")
}
@@ -160,7 +160,7 @@ func TestPostStoreDelete(t *testing.T) {
}
etag2 := (<-store.Post().GetEtag(o1.ChannelId)).Data.(string)
if strings.Index(etag2, model.ETAG_ROOT_VERSION+"."+o1.Id) != 0 {
if strings.Index(etag2, model.CurrentVersion+"."+o1.Id) != 0 {
t.Fatal("Invalid Etag")
}
}

Просмотреть файл

@@ -39,6 +39,7 @@ type SqlStore struct {
audit AuditStore
session SessionStore
oauth OAuthStore
system SystemStore
}
func NewSqlStore() Store {
@@ -56,9 +57,30 @@ func NewSqlStore() Store {
utils.Cfg.SqlSettings.Trace)
}
schemaVersion := sqlStore.GetCurrentSchemaVersion()
// If the version is already set then we are potentially in an 'upgrade needed' state
if schemaVersion != "" {
// Check to see if it's the most current database schema version
if !model.IsCurrentVersion(schemaVersion) {
// If we are upgrading from the previous version then print a warning and continue
if model.IsPreviousVersion(schemaVersion) {
l4g.Warn("The database schema version of " + schemaVersion + " appears to be out of date")
l4g.Warn("Attempting to upgrade the database schema version to " + model.CurrentVersion)
} else {
// If this is an 'upgrade needed' state but the user is attempting to skip a version then halt the world
l4g.Critical("The database schema version of " + schemaVersion + " cannot be upgraded. You must not skip a version.")
time.Sleep(time.Second)
panic("The database schema version of " + schemaVersion + " cannot be upgraded. You must not skip a version.")
}
}
}
// Temporary upgrade code, remove after 0.8.0 release
if sqlStore.DoesColumnExist("Sessions", "AltId") {
sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions")
if sqlStore.DoesTableExist("Sessions") {
if sqlStore.DoesColumnExist("Sessions", "AltId") {
sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions")
}
}
sqlStore.team = NewSqlTeamStore(sqlStore)
@@ -68,6 +90,7 @@ func NewSqlStore() Store {
sqlStore.audit = NewSqlAuditStore(sqlStore)
sqlStore.session = NewSqlSessionStore(sqlStore)
sqlStore.oauth = NewSqlOAuthStore(sqlStore)
sqlStore.system = NewSqlSystemStore(sqlStore)
sqlStore.master.CreateTablesIfNotExists()
@@ -78,6 +101,7 @@ func NewSqlStore() Store {
sqlStore.audit.(*SqlAuditStore).UpgradeSchemaIfNeeded()
sqlStore.session.(*SqlSessionStore).UpgradeSchemaIfNeeded()
sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded()
sqlStore.system.(*SqlSystemStore).UpgradeSchemaIfNeeded()
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
@@ -86,6 +110,17 @@ func NewSqlStore() Store {
sqlStore.audit.(*SqlAuditStore).CreateIndexesIfNotExists()
sqlStore.session.(*SqlSessionStore).CreateIndexesIfNotExists()
sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists()
sqlStore.system.(*SqlSystemStore).CreateIndexesIfNotExists()
if model.IsPreviousVersion(schemaVersion) {
sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion})
l4g.Warn("The database schema has been upgraded to version " + model.CurrentVersion)
}
if schemaVersion == "" {
sqlStore.system.Save(&model.System{Name: "Version", Value: model.CurrentVersion})
l4g.Info("The database schema has been set to version " + model.CurrentVersion)
}
return sqlStore
}
@@ -94,12 +129,12 @@ func setupConnection(con_type string, driver string, dataSource string, maxIdle
db, err := dbsql.Open(driver, dataSource)
if err != nil {
l4g.Critical("Failed to open sql connection to '%v' err:%v", dataSource, err)
l4g.Critical("Failed to open sql connection to err:%v", err)
time.Sleep(time.Second)
panic("Failed to open sql connection" + err.Error())
}
l4g.Info("Pinging sql %v database at '%v'", con_type, dataSource)
l4g.Info("Pinging sql %v database", con_type)
err = db.Ping()
if err != nil {
l4g.Critical("Failed to ping db err:%v", err)
@@ -131,6 +166,56 @@ func setupConnection(con_type string, driver string, dataSource string, maxIdle
return dbmap
}
func (ss SqlStore) GetCurrentSchemaVersion() string {
version, _ := ss.GetMaster().SelectStr("SELECT Value FROM Systems WHERE Name='Version'")
return version
}
func (ss SqlStore) DoesTableExist(tableName string) bool {
if utils.Cfg.SqlSettings.DriverName == "postgres" {
count, err := ss.GetMaster().SelectInt(
`SELECT count(relname) FROM pg_class WHERE relname=$1`,
strings.ToLower(tableName),
)
if err != nil {
l4g.Critical("Failed to check if table exists %v", err)
time.Sleep(time.Second)
panic("Failed to check if table exists " + err.Error())
}
return count > 0
} else if utils.Cfg.SqlSettings.DriverName == "mysql" {
count, err := ss.GetMaster().SelectInt(
`SELECT
COUNT(0) AS table_exists
FROM
information_schema.TABLES
WHERE
TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = ?
`,
tableName,
)
if err != nil {
l4g.Critical("Failed to check if table exists %v", err)
time.Sleep(time.Second)
panic("Failed to check if table exists " + err.Error())
}
return count > 0
} else {
l4g.Critical("Failed to check if column exists because of missing driver")
time.Sleep(time.Second)
panic("Failed to check if column exists because of missing driver")
}
}
func (ss SqlStore) DoesColumnExist(tableName string, columnName string) bool {
if utils.Cfg.SqlSettings.DriverName == "postgres" {
count, err := ss.GetMaster().SelectInt(
@@ -144,6 +229,10 @@ func (ss SqlStore) DoesColumnExist(tableName string, columnName string) bool {
)
if err != nil {
if err.Error() == "pq: relation \""+strings.ToLower(tableName)+"\" does not exist" {
return false
}
l4g.Critical("Failed to check if column exists %v", err)
time.Sleep(time.Second)
panic("Failed to check if column exists " + err.Error())
@@ -376,6 +465,10 @@ func (ss SqlStore) OAuth() OAuthStore {
return ss.oauth
}
func (ss SqlStore) System() SystemStore {
return ss.system
}
type mattermConverter struct{}
func (me mattermConverter) ToDb(val interface{}) (interface{}, error) {

92
store/sql_system_store.go Обычный файл
Просмотреть файл

@@ -0,0 +1,92 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package store
import (
"github.com/mattermost/platform/model"
)
type SqlSystemStore struct {
*SqlStore
}
func NewSqlSystemStore(sqlStore *SqlStore) SystemStore {
s := &SqlSystemStore{sqlStore}
for _, db := range sqlStore.GetAllConns() {
table := db.AddTableWithName(model.System{}, "Systems").SetKeys(false, "Name")
table.ColMap("Name").SetMaxSize(64)
table.ColMap("Value").SetMaxSize(1024)
}
return s
}
func (s SqlSystemStore) UpgradeSchemaIfNeeded() {
}
func (s SqlSystemStore) CreateIndexesIfNotExists() {
}
func (s SqlSystemStore) Save(system *model.System) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if err := s.GetMaster().Insert(system); err != nil {
result.Err = model.NewAppError("SqlSystemStore.Save", "We encounted an error saving the system property", "")
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlSystemStore) Update(system *model.System) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if _, err := s.GetMaster().Update(system); err != nil {
result.Err = model.NewAppError("SqlSystemStore.Save", "We encounted an error updating the system property", "")
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (s SqlSystemStore) Get() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var systems []model.System
props := make(model.StringMap)
if _, err := s.GetReplica().Select(&systems, "SELECT * FROM Systems"); err != nil {
result.Err = model.NewAppError("SqlSystemStore.Get", "We encounted an error finding the system properties", "")
} else {
for _, prop := range systems {
props[prop.Name] = prop.Value
}
result.Data = props
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}

33
store/sql_system_store_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,33 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package store
import (
"github.com/mattermost/platform/model"
"testing"
)
func TestSqlSystemStore(t *testing.T) {
Setup()
system := &model.System{Name: model.NewId(), Value: "value"}
Must(store.System().Save(system))
result := <-store.System().Get()
systems := result.Data.(model.StringMap)
if systems[system.Name] != system.Value {
t.Fatal()
}
system.Value = "value2"
Must(store.System().Update(system))
result2 := <-store.System().Get()
systems2 := result2.Data.(model.StringMap)
if systems2[system.Name] != system.Value {
t.Fatal()
}
}

Просмотреть файл

@@ -325,9 +325,9 @@ func (s SqlUserStore) GetEtagForProfiles(teamId string) StoreChannel {
updateAt, err := s.GetReplica().SelectInt("SELECT UpdateAt FROM Users WHERE TeamId = :TeamId ORDER BY UpdateAt DESC LIMIT 1", map[string]interface{}{"TeamId": teamId})
if err != nil {
result.Data = fmt.Sprintf("%v.%v", model.ETAG_ROOT_VERSION, model.GetMillis())
result.Data = fmt.Sprintf("%v.%v", model.CurrentVersion, model.GetMillis())
} else {
result.Data = fmt.Sprintf("%v.%v", model.ETAG_ROOT_VERSION, updateAt)
result.Data = fmt.Sprintf("%v.%v", model.CurrentVersion, updateAt)
}
storeChannel <- result

Просмотреть файл

@@ -35,6 +35,7 @@ type Store interface {
Audit() AuditStore
Session() SessionStore
OAuth() OAuthStore
System() SystemStore
Close()
}
@@ -130,3 +131,9 @@ type OAuthStore interface {
GetAccessDataByAuthCode(authCode string) StoreChannel
RemoveAccessData(token string) StoreChannel
}
type SystemStore interface {
Save(system *model.System) StoreChannel
Update(system *model.System) StoreChannel
Get() StoreChannel
}

Просмотреть файл

@@ -6,11 +6,14 @@ package utils
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
l4g "code.google.com/p/log4go"
"github.com/mattermost/platform/model"
)
const (
@@ -20,139 +23,9 @@ const (
LOG_ROTATE_SIZE = 10000
)
type ServiceSettings struct {
SiteName string
Mode string
AllowTesting bool
UseSSL bool
Port string
Version string
InviteSalt string
PublicLinkSalt string
ResetSalt string
AnalyticsUrl string
UseLocalStorage bool
StorageDirectory string
AllowedLoginAttempts int
DisableEmailSignUp bool
EnableOAuthServiceProvider bool
}
type SSOSetting struct {
Allow bool
Secret string
Id string
Scope string
AuthEndpoint string
TokenEndpoint string
UserApiEndpoint string
}
type SqlSettings struct {
DriverName string
DataSource string
DataSourceReplicas []string
MaxIdleConns int
MaxOpenConns int
Trace bool
AtRestEncryptKey string
}
type LogSettings struct {
ConsoleEnable bool
ConsoleLevel string
FileEnable bool
FileLevel string
FileFormat string
FileLocation string
}
type AWSSettings struct {
S3AccessKeyId string
S3SecretAccessKey string
S3Bucket string
S3Region string
}
type ImageSettings struct {
ThumbnailWidth uint
ThumbnailHeight uint
PreviewWidth uint
PreviewHeight uint
ProfileWidth uint
ProfileHeight uint
InitialFont string
}
type EmailSettings struct {
ByPassEmail bool
SMTPUsername string
SMTPPassword string
SMTPServer string
UseTLS bool
UseStartTLS bool
FeedbackEmail string
FeedbackName string
ApplePushServer string
ApplePushCertPublic string
ApplePushCertPrivate string
}
type RateLimitSettings struct {
UseRateLimiter bool
PerSec int
MemoryStoreSize int
VaryByRemoteAddr bool
VaryByHeader string
}
type PrivacySettings struct {
ShowEmailAddress bool
ShowPhoneNumber bool
ShowSkypeId bool
ShowFullName bool
}
type ClientSettings struct {
SegmentDeveloperKey string
GoogleDeveloperKey string
}
type TeamSettings struct {
MaxUsersPerTeam int
AllowPublicLink bool
AllowValetDefault bool
TourLink string
DefaultThemeColor string
DisableTeamCreation bool
RestrictCreationToDomains string
}
type Config struct {
LogSettings LogSettings
ServiceSettings ServiceSettings
SqlSettings SqlSettings
AWSSettings AWSSettings
ImageSettings ImageSettings
EmailSettings EmailSettings
RateLimitSettings RateLimitSettings
PrivacySettings PrivacySettings
ClientSettings ClientSettings
TeamSettings TeamSettings
SSOSettings map[string]SSOSetting
}
func (o *Config) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
return ""
} else {
return string(b)
}
}
var Cfg *Config = &Config{}
var Cfg *model.Config = &model.Config{}
var CfgLastModified int64 = 0
var CfgFileName string = ""
var ClientProperties map[string]string = map[string]string{}
var SanitizeOptions map[string]bool = map[string]bool{}
@@ -184,14 +57,14 @@ func FindDir(dir string) string {
}
func ConfigureCmdLineLog() {
ls := LogSettings{}
ls := model.LogSettings{}
ls.ConsoleEnable = true
ls.ConsoleLevel = "ERROR"
ls.FileEnable = false
configureLog(&ls)
}
func configureLog(s *LogSettings) {
func configureLog(s *model.LogSettings) {
l4g.Close()
@@ -207,12 +80,11 @@ func configureLog(s *LogSettings) {
}
if s.FileEnable {
if s.FileFormat == "" {
s.FileFormat = "[%D %T] [%L] %M"
}
if s.FileLocation == "" {
s.FileLocation = FindDir("logs") + "mattermost.log"
var fileFormat = s.FileFormat
if fileFormat == "" {
fileFormat = "[%D %T] [%L] %M"
}
level := l4g.DEBUG
@@ -222,14 +94,36 @@ func configureLog(s *LogSettings) {
level = l4g.ERROR
}
flw := l4g.NewFileLogWriter(s.FileLocation, false)
flw.SetFormat(s.FileFormat)
flw := l4g.NewFileLogWriter(GetLogFileLocation(s.FileLocation), false)
flw.SetFormat(fileFormat)
flw.SetRotate(true)
flw.SetRotateLines(LOG_ROTATE_SIZE)
l4g.AddFilter("file", level, flw)
}
}
func GetLogFileLocation(fileLocation string) string {
if fileLocation == "" {
return FindDir("logs") + "mattermost.log"
} else {
return fileLocation
}
}
func SaveConfig(fileName string, config *model.Config) *model.AppError {
b, err := json.MarshalIndent(config, "", " ")
if err != nil {
return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error())
}
err = ioutil.WriteFile(fileName, b, 0644)
if err != nil {
return model.NewAppError("SaveConfig", "An error occurred while saving the file to "+fileName, err.Error())
}
return nil
}
// LoadConfig will try to search around for the corresponding config file.
// It will search /tmp/fileName then attempt ./config/fileName,
// then ../config/fileName and last it will look at fileName
@@ -243,7 +137,7 @@ func LoadConfig(fileName string) {
}
decoder := json.NewDecoder(file)
config := Config{}
config := model.Config{}
err = decoder.Decode(&config)
if err != nil {
panic("Error decoding config file=" + fileName + ", err=" + err.Error())
@@ -253,6 +147,7 @@ func LoadConfig(fileName string) {
panic("Error getting config info file=" + fileName + ", err=" + err.Error())
} else {
CfgLastModified = info.ModTime().Unix()
CfgFileName = fileName
}
configureLog(&config.LogSettings)
@@ -262,7 +157,7 @@ func LoadConfig(fileName string) {
ClientProperties = getClientProperties(Cfg)
}
func getSanitizeOptions(c *Config) map[string]bool {
func getSanitizeOptions(c *model.Config) map[string]bool {
options := map[string]bool{}
options["fullname"] = c.PrivacySettings.ShowFullName
options["email"] = c.PrivacySettings.ShowEmailAddress
@@ -272,10 +167,14 @@ func getSanitizeOptions(c *Config) map[string]bool {
return options
}
func getClientProperties(c *Config) map[string]string {
func getClientProperties(c *model.Config) map[string]string {
props := make(map[string]string)
props["Version"] = c.ServiceSettings.Version
props["Version"] = model.CurrentVersion
props["BuildNumber"] = model.BuildNumber
props["BuildDate"] = model.BuildDate
props["BuildHash"] = model.BuildHash
props["SiteName"] = c.ServiceSettings.SiteName
props["ByPassEmail"] = strconv.FormatBool(c.EmailSettings.ByPassEmail)
props["FeedbackEmail"] = c.EmailSettings.FeedbackEmail

Просмотреть файл

@@ -2,35 +2,58 @@
// See License.txt for license information.
var AdminSidebar = require('./admin_sidebar.jsx');
var EmailTab = require('./email_settings.jsx');
var JobsTab = require('./jobs_settings.jsx');
var AdminStore = require('../../stores/admin_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
var EmailSettingsTab = require('./email_settings.jsx');
var LogSettingsTab = require('./log_settings.jsx');
var LogsTab = require('./logs.jsx');
var Navbar = require('../../components/navbar.jsx');
export default class AdminController extends React.Component {
constructor(props) {
super(props);
this.selectTab = this.selectTab.bind(this);
this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
this.state = {
config: null,
selected: 'email_settings'
};
}
componentDidMount() {
AdminStore.addConfigChangeListener(this.onConfigListenerChange);
AsyncClient.getConfig();
}
componentWillUnmount() {
AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
}
onConfigListenerChange() {
this.setState({
config: AdminStore.getConfig(),
selected: this.state.selected
});
}
selectTab(tab) {
this.setState({selected: tab});
}
render() {
var tab = '';
var tab = <LoadingScreen />;
if (this.state.selected === 'email_settings') {
tab = <EmailTab />;
} else if (this.state.selected === 'job_settings') {
tab = <JobsTab />;
} else if (this.state.selected === 'logs') {
tab = <LogsTab />;
if (this.state.config != null) {
if (this.state.selected === 'email_settings') {
tab = <EmailSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'log_settings') {
tab = <LogSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'logs') {
tab = <LogsTab />;
}
}
return (
@@ -45,7 +68,6 @@ export default class AdminController extends React.Component {
/>
<div className='inner__wrap channel__wrap'>
<div className='row header'>
<Navbar teamDisplayName='Admin Console' />
</div>
<div className='row main'>
<div

Просмотреть файл

@@ -0,0 +1,102 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Utils = require('../../utils/utils.jsx');
var Client = require('../../utils/client.jsx');
var TeamStore = require('../../stores/team_store.jsx');
var Constants = require('../../utils/constants.jsx');
function getStateFromStores() {
return {currentTeam: TeamStore.getCurrent()};
}
export default class AdminNavbarDropdown extends React.Component {
constructor(props) {
super(props);
this.blockToggle = false;
this.handleLogoutClick = this.handleLogoutClick.bind(this);
this.state = getStateFromStores();
}
handleLogoutClick(e) {
e.preventDefault();
Client.logout();
}
componentDidMount() {
$(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
this.blockToggle = true;
setTimeout(() => {
this.blockToggle = false;
}, 100);
});
}
componentWillUnmount() {
$(React.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
}
render() {
return (
<ul className='nav navbar-nav navbar-right'>
<li
ref='dropdown'
className='dropdown'
>
<a
href='#'
className='dropdown-toggle'
data-toggle='dropdown'
role='button'
aria-expanded='false'
>
<span
className='dropdown__icon'
dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}}
/>
</a>
<ul
className='dropdown-menu'
role='menu'
>
<li>
<a
href={Utils.getWindowLocationOrigin() + '/' + this.state.currentTeam.name}
>
{'Switch to ' + this.state.currentTeam.display_name}
</a>
</li>
<li>
<a
href='#'
onClick={this.handleLogoutClick}
>
{'Logout'}
</a>
</li>
<li className='divider'></li>
<li>
<a
target='_blank'
href='/static/help/help.html'
>
{'Help'}
</a>
</li>
<li>
<a
target='_blank'
href='/static/help/report_problem.html'
>
{'Report a Problem'}
</a>
</li>
</ul>
</li>
</ul>
);
}
}

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var SidebarHeader = require('../sidebar_header.jsx');
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -14,7 +14,8 @@ export default class AdminSidebar extends React.Component {
};
}
handleClick(name) {
handleClick(name, e) {
e.preventDefault();
this.props.selectTab(name);
}
@@ -27,171 +28,45 @@ export default class AdminSidebar extends React.Component {
}
componentDidMount() {
$('.nav__menu-item').on('click', function clickme(e) {
e.preventDefault();
$(this).closest('.sidebar--collapsable').find('.nav__menu-item').removeClass('active');
$(this).addClass('active');
$(this).closest('.sidebar--collapsable').find('.nav__sub-menu').addClass('hide');
$(this).next('.nav__sub-menu').removeClass('hide');
});
$('.nav__sub-menu a').on('click', function clickme(e) {
e.preventDefault();
$(this).closest('.nav__sub-menu').find('a').removeClass('active');
$(this).addClass('active');
});
$('.nav__sub-menu-item').on('click', function clickme(e) {
e.preventDefault();
$(this).closest('.sidebar--collapsable').find('.nav__inner-menu').addClass('hide');
$(this).closest('li').next('li').find('.nav__inner-menu').removeClass('hide');
$(this).closest('li').next('li').find('.nav__inner-menu li:first a').addClass('active');
});
$('.nav__inner-menu a').on('click', function clickme() {
$(this).closest('.nav__inner-menu').closest('li').prev('li').find('a').addClass('active');
});
$('.nav__sub-menu .menu__close').on('click', function close() {
var menuItem = $(this).closest('li');
menuItem.next('li').remove();
menuItem.remove();
});
}
render() {
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
<SidebarHeader
teamDisplayName='Admin Console'
teamType='I'
/>
<AdminSidebarHeader />
<ul className='nav nav-pills nav-stacked'>
<li>
<a href='#'
className='nav__menu-item active'
>
<span className='icon fa fa-gear'></span> <span>{'Basic Settings'}</span></a>
<ul className='nav nav__sub-menu'>
<li>
<a
href='#'
className={this.isSelected('email_settings')}
onClick={this.handleClick.bind(null, 'email_settings')}
onClick={this.handleClick.bind(this, 'email_settings')}
>
{'Email Settings'}
</a>
</li>
<li>
<a
href='#'
className={this.isSelected('log_settings')}
onClick={this.handleClick.bind(this, 'log_settings')}
>
{'Log Settings'}
</a>
</li>
<li>
<a
href='#'
className={this.isSelected('logs')}
onClick={this.handleClick.bind(null, 'logs')}
onClick={this.handleClick.bind(this, 'logs')}
>
{'Logs'}
</a>
</li>
</ul>
</li>
<li>
<a
href='#'
className='nav__menu-item'
>
<span className='icon fa fa-gear'></span> <span>{'Jobs'}</span>
</a>
<ul className='nav nav__sub-menu hide'>
<li>
<a
href='#'
className={this.isSelected('job_settings')}
onClick={this.handleClick.bind(null, 'job_settings')}
>
{'Job Settings'}
</a>
</li>
</ul>
</li>
<li>
<a
href='#'
className='nav__menu-item'
>
<span className='icon fa fa-gear'></span>
<span>{'Team Settings (306)'}</span>
<span className='menu-icon--right'>
<i className='fa fa-plus'></i>
</span>
</a>
<ul className='nav nav__sub-menu hide'>
<li>
<a
href='#'
className='nav__sub-menu-item active'
>
{'Adal '}
<span className='menu-icon--right menu__close'>{'x'}</span>
</a>
</li>
<li>
<ul className='nav nav__inner-menu'>
<li>
<a
href='#'
className='active'
>
{'- Users'}
</a>
</li>
<li><a href='#'>{'- View Statistics'}</a></li>
<li>
<a href='#'>
{'- View Audit Log'}
<span className='badge pull-right small'>{'1'}</span>
</a>
</li>
</ul>
</li>
<li>
<a
href='#'
className='nav__sub-menu-item'
>
{'Boole '}
<span className='menu-icon--right menu__close'>{'x'}</span>
</a>
</li>
<li>
<ul className='nav nav__inner-menu hide'>
<li>
<a
href='#'
className='active'
>
{'- Users'}
</a>
</li>
<li><a href='#'>{'- View Statistics'}</a></li>
<li>
<a href='#'>
{'- View Audit Log'}
<span className='badge pull-right small'>{'1'}</span>
</a>
</li>
</ul>
</li>
<li>
<span
data-toggle='modal'
data-target='#select-team'
className='nav-more'
>
{'Select a team'}
</span>
</li>
</ul>
</li>
</ul>
</div>
</div>

Просмотреть файл

@@ -0,0 +1,60 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var AdminNavbarDropdown = require('./admin_navbar_dropdown.jsx');
var UserStore = require('../../stores/user_store.jsx');
export default class SidebarHeader extends React.Component {
constructor(props) {
super(props);
this.toggleDropdown = this.toggleDropdown.bind(this);
this.state = {};
}
toggleDropdown(e) {
e.preventDefault();
if (this.refs.dropdown.blockToggle) {
this.refs.dropdown.blockToggle = false;
return;
}
$('.team__header').find('.dropdown-toggle').dropdown('toggle');
}
render() {
var me = UserStore.getCurrentUser();
var profilePicture = null;
if (!me) {
return null;
}
if (me.last_picture_update) {
profilePicture = (
<img
className='user__picture'
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
/>
);
}
return (
<div className='team__header theme'>
<a
href='#'
onClick={this.toggleDropdown}
>
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
<div className='team__name'>{'System Console'}</div>
</div>
</a>
<AdminNavbarDropdown ref='dropdown' />
</div>
);
}
}

Просмотреть файл

@@ -1,183 +0,0 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
export default class Jobs extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
render() {
return (
<div className='wrapper--fixed'>
<h3>{' ************** JOB Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='email'
>
{'Bypass Email: '}
<a
href='#'
data-trigger='hover click'
data-toggle='popover'
data-position='bottom'
data-content={'Here\'s some more help text inside a popover for the Bypass Email field just to show how popovers look.'}
>
{'(?)'}
</a>
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='byPassEmail'
value='option1'
/>
{'True'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='byPassEmail'
value='option2'
/>
{'False'}
</label>
<p className='help-text'>{'This is some sample help text for the Bypass Email field'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='smtpUsername'
>
{'SMTP Username:'}
</label>
<div className='col-sm-8'>
<input
type='email'
className='form-control'
id='smtpUsername'
placeholder='Enter your SMTP username'
value=''
/>
<div className='help-text'>
<div className='alert alert-warning'><i className='fa fa-warning'></i>{' This is some error text for the Bypass Email field'}</div>
</div>
<p className='help-text'>{'This is some sample help text for the SMTP username field'}</p>
</div>
</div>
<div
className='panel-group'
id='accordion'
role='tablist'
aria-multiselectable='true'
>
<div className='panel panel-default'>
<div
className='panel-heading'
role='tab'
id='headingOne'
>
<h3 className='panel-title'>
<a
className='collapsed'
role='button'
data-toggle='collapse'
data-parent='#accordion'
href='#collapseOne'
aria-expanded='true'
aria-controls='collapseOne'
>
{'Advanced Settings '}
<i className='fa fa-plus'></i>
<i className='fa fa-minus'></i>
</a>
</h3>
</div>
<div
id='collapseOne'
className='panel-collapse collapse'
role='tabpanel'
aria-labelledby='headingOne'
>
<div className='panel-body'>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='feedbackUsername'
>
{'Apple push server:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='feedbackUsername'
placeholder='Enter your Apple push server'
value=''
/>
<p className='help-text'>{'This is some sample help text for the Apple push server field'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='feedbackUsername'
>
{'Apple push certificate public:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='feedbackUsername'
placeholder='Enter your public apple push certificate'
value=''
/>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='feedbackUsername'
>
{'Apple push certificate private:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='feedbackUsername'
placeholder='Enter your private apple push certificate'
value=''
/>
</div>
</div>
</div>
</div>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
<button
type='submit'
className='btn btn-primary'
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}

Просмотреть файл

@@ -0,0 +1,261 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../../utils/client.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
export default class LogSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
saveNeeded: false,
serverError: null
};
}
handleChange() {
this.setState({saveNeeded: true, serverError: this.state.serverError});
}
handleSubmit(e) {
e.preventDefault();
$('#save-button').button('loading');
var config = this.props.config;
config.LogSettings.ConsoleEnable = React.findDOMNode(this.refs.consoleEnable).checked;
config.LogSettings.ConsoleLevel = React.findDOMNode(this.refs.consoleLevel).value;
config.LogSettings.FileEnable = React.findDOMNode(this.refs.fileEnable).checked;
config.LogSettings.FileLevel = React.findDOMNode(this.refs.fileLevel).value;
config.LogSettings.FileLocation = React.findDOMNode(this.refs.fileLocation).value.trim();
config.LogSettings.FileFormat = React.findDOMNode(this.refs.fileFormat).value.trim();
Client.saveConfig(
config,
() => {
AsyncClient.getConfig();
this.setState({serverError: null, saveNeeded: false});
$('#save-button').button('reset');
},
(err) => {
this.setState({serverError: err.message, saveNeeded: true});
$('#save-button').button('reset');
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var saveClass = 'btn';
if (this.state.saveNeeded) {
saveClass = 'btn btn-primary';
}
return (
<div className='wrapper--fixed'>
<h3>{'Log Settings'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='consoleEnable'
>
{'Log To the Console: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='consoleEnable'
value='true'
ref='consoleEnable'
defaultChecked={this.props.config.LogSettings.ConsoleEnable}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='consoleEnable'
value='false'
defaultChecked={!this.props.config.LogSettings.ConsoleEnable}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true then the server will output messages to the standard output stream (stdout).'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='consoleLevel'
>
{'Console Log Level:'}
</label>
<div className='col-sm-8'>
<select
className='form-control'
id='consoleLevel'
ref='consoleLevel'
defaultValue={this.props.config.LogSettings.consoleLevel}
onChange={this.handleChange}
>
<option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option>
<option value='ERROR'>{'ERROR'}</option>
</select>
<p className='help-text'>{'This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
>
{'Log To File: '}
</label>
<div className='col-sm-8'>
<label className='radio-inline'>
<input
type='radio'
name='fileEnable'
ref='fileEnable'
value='true'
defaultChecked={this.props.config.LogSettings.FileEnable}
onChange={this.handleChange}
/>
{'true'}
</label>
<label className='radio-inline'>
<input
type='radio'
name='fileEnable'
value='false'
defaultChecked={!this.props.config.LogSettings.FileEnable}
onChange={this.handleChange}
/>
{'false'}
</label>
<p className='help-text'>{'Typically set to true in production. When true log files are written to the file specified in file location field below.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='fileLevel'
>
{'File Log Level:'}
</label>
<div className='col-sm-8'>
<select
className='form-control'
id='fileLevel'
ref='fileLevel'
defaultValue={this.props.config.LogSettings.FileLevel}
onChange={this.handleChange}
>
<option value='DEBUG'>{'DEBUG'}</option>
<option value='INFO'>{'INFO'}</option>
<option value='ERROR'>{'ERROR'}</option>
</select>
<p className='help-text'>{'This setting determines the level of detail at which log events are written to the file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers debugging issues working on debugging issues.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='fileLocation'
>
{'File Location:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='fileLocation'
ref='fileLocation'
placeholder='Enter your file location'
defaultValue={this.props.config.LogSettings.FileLocation}
onChange={this.handleChange}
/>
<p className='help-text'>{'File to which log files are written. If blank, will be set to ./logs/mattermost.log. Log rotation is enabled and new files may be created in the same directory.'}</p>
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
htmlFor='fileFormat'
>
{'File Format:'}
</label>
<div className='col-sm-8'>
<input
type='text'
className='form-control'
id='fileFormat'
ref='fileFormat'
placeholder='Enter your file format'
defaultValue={this.props.config.LogSettings.FileFormat}
onChange={this.handleChange}
/>
<p className='help-text'>
{'Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'}
<div className='help-text'>
<table
className='table-bordered'
cellPadding='5'
>
<tr><td className='help-text'>{'%T'}</td><td className='help-text'>{'Time (15:04:05 MST)'}</td></tr>
<tr><td className='help-text'>{'%D'}</td><td className='help-text'>{'Date (2006/01/02)'}</td></tr>
<tr><td className='help-text'>{'%d'}</td><td className='help-text'>{'Date (01/02/06)'}</td></tr>
<tr><td className='help-text'>{'%L'}</td><td className='help-text'>{'Level (DEBG, INFO, EROR)'}</td></tr>
<tr><td className='help-text'>{'%S'}</td><td className='help-text'>{'Source'}</td></tr>
<tr><td className='help-text'>{'%M'}</td><td className='help-text'>{'Message'}</td></tr>
</table>
</div>
</p>
</div>
</div>
<div className='form-group'>
<div className='col-sm-12'>
{serverError}
<button
disabled={!this.state.saveNeeded}
type='submit'
className={saveClass}
onClick={this.handleSubmit}
id='save-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Saving Config...'}
>
{'Save'}
</button>
</div>
</div>
</form>
</div>
);
}
}
LogSettings.propTypes = {
config: React.PropTypes.object
};

Просмотреть файл

@@ -21,9 +21,11 @@ export default class Logs extends React.Component {
AdminStore.addLogChangeListener(this.onLogListenerChange);
AsyncClient.getLogs();
}
componentWillUnmount() {
AdminStore.removeLogChangeListener(this.onLogListenerChange);
}
onLogListenerChange() {
this.setState({
logs: AdminStore.getLogs()

Просмотреть файл

@@ -111,7 +111,7 @@ export default class ChannelHeader extends React.Component {
const popoverContent = React.renderToString(<MessageWrapper message={channel.description}/>);
let channelTitle = channel.display_name;
const currentId = UserStore.getCurrentId();
const isAdmin = this.state.memberChannel.roles.indexOf('admin') > -1 || this.state.memberTeam.roles.indexOf('admin') > -1;
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.memberTeam.roles);
const isDirect = (this.state.channel.type === 'D');
if (isDirect) {

Просмотреть файл

@@ -121,7 +121,7 @@ export default class ChannelInviteModal extends React.Component {
var currentMember = ChannelStore.getCurrentMember();
var isAdmin = false;
if (currentMember) {
isAdmin = currentMember.roles.indexOf('admin') > -1 || UserStore.getCurrentUser().roles.indexOf('admin') > -1;
isAdmin = utils.isAdmin(currentMember.roles) || utils.isAdmin(UserStore.getCurrentUser().roles);
}
var content;

Просмотреть файл

@@ -130,7 +130,7 @@ export default class ChannelMembers extends React.Component {
const currentMember = ChannelStore.getCurrentMember();
let isAdmin = false;
if (currentMember) {
isAdmin = currentMember.roles.indexOf('admin') > -1 || UserStore.getCurrentUser().roles.indexOf('admin') > -1;
isAdmin = Utils.isAdmin(currentMember.roles) || Utils.isAdmin(UserStore.getCurrentUser().roles);
}
var memberList = null;

Просмотреть файл

@@ -2,6 +2,7 @@
// See License.txt for license information.
var UserStore = require('../stores/user_store.jsx');
const Utils = require('../utils/utils.jsx');
export default class MemberListItem extends React.Component {
constructor(props) {
@@ -26,7 +27,7 @@ export default class MemberListItem extends React.Component {
render() {
var member = this.props.member;
var isAdmin = this.props.isAdmin;
var isMemberAdmin = member.roles.indexOf('admin') > -1;
var isMemberAdmin = Utils.isAdmin(member.roles);
var timestamp = UserStore.getCurrentUser().update_at;
var invite;

Просмотреть файл

@@ -6,6 +6,7 @@ var client = require('../utils/client.jsx');
var asyncClient = require('../utils/async_client.jsx');
var ChannelStore = require('../stores/channel_store.jsx');
var LoadingScreen = require('./loading_screen.jsx');
var NewChannelFlow = require('./new_channel_flow.jsx');
function getStateFromStores() {
return {
@@ -25,6 +26,7 @@ export default class MoreChannels extends React.Component {
var initState = getStateFromStores();
initState.channelType = '';
initState.joiningChannel = -1;
initState.showNewChannelModal = false;
this.state = initState;
}
componentDidMount() {
@@ -66,6 +68,7 @@ export default class MoreChannels extends React.Component {
}
handleNewChannel() {
$(React.findDOMNode(this.refs.modal)).modal('hide');
this.setState({showNewChannelModal: true});
}
render() {
var serverError;
@@ -148,20 +151,22 @@ export default class MoreChannels extends React.Component {
className='close'
data-dismiss='modal'
>
<span aria-hidden='true'>&times;</span>
<span className='sr-only'>Close</span>
<span aria-hidden='true'>{'×'}</span>
<span className='sr-only'>{'Close'}</span>
</button>
<h4 className='modal-title'>More Channels</h4>
<h4 className='modal-title'>{'More Channels'}</h4>
<button
data-toggle='modal'
data-target='#new_channel'
data-channeltype={this.state.channelType}
type='button'
className='btn btn-primary channel-create-btn'
onClick={this.handleNewChannel}
>
Create New Channel
{'Create New Channel'}
</button>
<NewChannelFlow
show={this.state.showNewChannelModal}
channelType={this.state.channelType}
onModalDismissed={() => this.setState({showNewChannelModal: false})}
/>
</div>
<div className='modal-body'>
{moreChannels}
@@ -173,7 +178,7 @@ export default class MoreChannels extends React.Component {
className='btn btn-default'
data-dismiss='modal'
>
Close
{'Close'}
</button>
</div>
</div>

Просмотреть файл

@@ -8,6 +8,7 @@ var ChannelStore = require('../stores/channel_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var MessageWrapper = require('./message_wrapper.jsx');
var NotifyCounts = require('./notify_counts.jsx');
const Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -335,7 +336,7 @@ export default class Navbar extends React.Component {
options={{singleline: true, mentionHighlight: false}}
/>
);
isAdmin = this.state.member.roles.indexOf('admin') > -1;
isAdmin = Utils.isAdmin(this.state.member.roles);
if (channel.type === 'O') {
channelTitle = channel.display_name;

Просмотреть файл

@@ -30,12 +30,12 @@ export default class NavbarDropdown extends React.Component {
UserStore.addTeamsChangeListener(this.onListenerChange);
TeamStore.addChangeListener(this.onListenerChange);
$(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', function resetDropdown() {
$(React.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
this.blockToggle = true;
setTimeout(function blockTimeout() {
setTimeout(() => {
this.blockToggle = false;
}.bind(this), 100);
}.bind(this));
}, 100);
});
}
componentWillUnmount() {
UserStore.removeTeamsChangeListener(this.onListenerChange);
@@ -53,12 +53,16 @@ export default class NavbarDropdown extends React.Component {
var teamLink = '';
var inviteLink = '';
var manageLink = '';
var sysAdminLink = '';
var adminDivider = '';
var currentUser = UserStore.getCurrentUser();
var isAdmin = false;
var isSystemAdmin = false;
var teamSettings = null;
if (currentUser != null) {
isAdmin = currentUser.roles.indexOf('admin') > -1;
isAdmin = Utils.isAdmin(currentUser.roles);
isSystemAdmin = Utils.isInRole(currentUser.roles, 'system_admin');
inviteLink = (
<li>
@@ -67,7 +71,7 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#invite_member'
>
Invite New Member
{'Invite New Member'}
</a>
</li>
);
@@ -82,7 +86,7 @@ export default class NavbarDropdown extends React.Component {
data-title='Team Invite'
data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id}
>
Get Team Invite Link
{'Get Team Invite Link'}
</a>
</li>
);
@@ -97,19 +101,36 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#team_members'
>
Manage Team
{'Manage Team'}
</a>
</li>
);
adminDivider = (<li className='divider'></li>);
teamSettings = (
<li>
<a
href='#'
data-toggle='modal'
data-target='#team_settings'
>
{'Team Settings'}
</a>
</li>
);
}
if (isSystemAdmin) {
sysAdminLink = (
<li>
<a
href='/admin_console'
>
{'System Console'}
</a>
</li>
);
teamSettings = (<li>
<a
href='#'
data-toggle='modal'
data-target='#team_settings'
>
Team Settings
</a>
</li>);
}
var teams = [];
@@ -123,9 +144,9 @@ export default class NavbarDropdown extends React.Component {
);
if (this.state.teams.length > 1 && this.state.currentTeam) {
var curTeamName = this.state.currentTeam.name;
this.state.teams.forEach(function listTeams(teamName) {
this.state.teams.forEach((teamName) => {
if (teamName !== curTeamName) {
teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>Switch to {teamName}</a></li>);
teams.push(<li key={teamName}><a href={Utils.getWindowLocationOrigin() + '/' + teamName}>{'Switch to ' + teamName}</a></li>);
}
});
}
@@ -135,7 +156,7 @@ export default class NavbarDropdown extends React.Component {
target='_blank'
href={Utils.getWindowLocationOrigin() + '/signup_team'}
>
Create a New Team
{'Create a New Team'}
</a>
</li>);
@@ -167,21 +188,23 @@ export default class NavbarDropdown extends React.Component {
data-toggle='modal'
data-target='#user_settings'
>
Account Settings
{'Account Settings'}
</a>
</li>
{teamSettings}
{inviteLink}
{teamLink}
{manageLink}
<li>
<a
href='#'
onClick={this.handleLogoutClick}
>
Logout
{'Logout'}
</a>
</li>
{adminDivider}
{teamSettings}
{manageLink}
{sysAdminLink}
{teams}
<li className='divider'></li>
<li>
@@ -189,7 +212,7 @@ export default class NavbarDropdown extends React.Component {
target='_blank'
href='/static/help/help.html'
>
Help
{'Help'}
</a>
</li>
<li>
@@ -197,7 +220,7 @@ export default class NavbarDropdown extends React.Component {
target='_blank'
href='/static/help/report_problem.html'
>
Report a Problem
{'Report a Problem'}
</a>
</li>
</ul>

Просмотреть файл

@@ -107,8 +107,8 @@ export default class NewChannelModal extends React.Component {
{channelSwitchText}
</div>
<div className={displayNameClass}>
<label className='col-sm-2 form__label control-label'>{'Name'}</label>
<div className='col-sm-10'>
<label className='col-sm-3 form__label control-label'>{'Name'}</label>
<div className='col-sm-9'>
<input
onChange={this.handleChange}
type='text'
@@ -121,7 +121,7 @@ export default class NewChannelModal extends React.Component {
tabIndex='1'
/>
{displayNameError}
<p className='input__help'>
<p className='input__help dark'>
{'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
<a
href='#'
@@ -134,11 +134,11 @@ export default class NewChannelModal extends React.Component {
</div>
</div>
<div className='form-group less'>
<div className='col-sm-2'>
<div className='col-sm-3'>
<label className='form__label control-label'>{'Description'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
<div className='col-sm-10'>
<div className='col-sm-9'>
<textarea
className='form-control no-resize'
ref='channel_desc'
@@ -150,7 +150,7 @@ export default class NewChannelModal extends React.Component {
tabIndex='2'
/>
<p className='input__help'>
{'This is the purpose of your channel and helps others decide whether to join.'}
{'Description helps others decide whether to join this channel.'}
</p>
{serverError}
</div>

Просмотреть файл

@@ -35,9 +35,6 @@ export default class PostBody extends React.Component {
parseEmojis() {
twemoji.parse(React.findDOMNode(this), {size: Constants.EMOJI_SIZE});
this.getAllChildNodes(React.findDOMNode(this)).forEach((current) => {
global.window.emojify.run(current);
});
}
componentDidMount() {
@@ -154,17 +151,18 @@ export default class PostBody extends React.Component {
return (
<div className='post-body'>
{comment}
<p
<div
key={`${post.id}_message`}
id={`${post.id}_message`}
className={postClass}
>
{loading}
<span
ref='message_span'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
/>
</p>
</div>
{fileAttachmentHolder}
{embed}
</div>

Просмотреть файл

@@ -20,7 +20,7 @@ export default class PostInfo extends React.Component {
createDropdown() {
var post = this.props.post;
var isOwner = UserStore.getCurrentId() === post.user_id;
var isAdmin = UserStore.getCurrentUser().roles.indexOf('admin') > -1;
var isAdmin = utils.isAdmin(UserStore.getCurrentUser().roles);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || post.state === Constants.POST_DELETED) {
return '';
@@ -151,7 +151,10 @@ export default class PostInfo extends React.Component {
return (
<ul className='post-header post-info'>
<li className='post-header-col'>
<time className='post-profile-time'>
<time
className='post-profile-time'
title={new Date(post.create_at).toString()}
>
{utils.displayDateTime(post.create_at)}
</time>
</li>

Просмотреть файл

@@ -83,6 +83,7 @@ export default class PostList extends React.Component {
};
}
componentDidMount() {
window.onload = () => this.scrollToBottom();
if (this.props.isActive) {
this.activate();
this.loadFirstPosts(this.props.channelId);
@@ -419,7 +420,7 @@ export default class PostList extends React.Component {
var members = ChannelStore.getExtraInfo(channel.id).members;
for (var i = 0; i < members.length; i++) {
if (members[i].roles.indexOf('admin') > -1) {
if (utils.isAdmin(members[i].roles)) {
return members[i].username;
}
}

Просмотреть файл

@@ -193,7 +193,10 @@ export default class RhsComment extends React.Component {
<strong><UserProfile userId={post.user_id} /></strong>
</li>
<li className='post-header-col'>
<time className='post-right-comment-time'>
<time
className='post-profile-time'
title={new Date(post.create_at).toString()}
>
{Utils.displayCommentDateTime(post.create_at)}
</time>
</li>
@@ -204,7 +207,8 @@ export default class RhsComment extends React.Component {
<div className='post-body'>
<p className={postClass}>
{loading}
<span
<div
ref='message_holder'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
/>

Просмотреть файл

@@ -132,7 +132,14 @@ export default class RhsRootPost extends React.Component {
<div className='post__content'>
<ul className='post-header'>
<li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li>
<li className='post-header-col'><time className='post-right-root-time'>{utils.displayCommentDateTime(post.create_at)}</time></li>
<li className='post-header-col'>
<time
className='post-profile-time'
title={new Date(post.create_at).toString()}
>
{utils.displayCommentDateTime(post.create_at)}
</time>
</li>
<li className='post-header-col post-header__reply'>
<div className='dropdown'>
{ownerOptions}
@@ -140,7 +147,8 @@ export default class RhsRootPost extends React.Component {
</li>
</ul>
<div className='post-body'>
<p
<div
ref='message_holder'
onClick={TextFormatting.handleClick}
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(post.message)}}
/>

Просмотреть файл

@@ -12,7 +12,8 @@ export default class SidebarHeader extends React.Component {
this.state = {};
}
toggleDropdown() {
toggleDropdown(e) {
e.preventDefault();
if (this.refs.dropdown.blockToggle) {
this.refs.dropdown.blockToggle = false;
return;

Просмотреть файл

@@ -26,7 +26,7 @@ export default class SidebarRightMenu extends React.Component {
var isAdmin = false;
if (currentUser != null) {
isAdmin = currentUser.roles.indexOf('admin') > -1;
isAdmin = utils.isAdmin(currentUser.roles);
inviteLink = (
<li>

Просмотреть файл

@@ -251,6 +251,17 @@ export default class SecurityTab extends React.Component {
<div className='divider-dark first'/>
{passwordSection}
<div className='divider-dark'/>
<ul
className='section-min'
>
<li className='col-sm-10 section-title'>{'Version ' + global.window.config.Version}</li>
<li className='col-sm-7 section-describe'>
<div className='text-nowrap'>{'Build Number: ' + global.window.config.BuildNumber}</div>
<div className='text-nowrap'>{'Build Date: ' + global.window.config.BuildDate}</div>
<div className='text-nowrap'>{'Build Hash: ' + global.window.config.BuildHash}</div>
</li>
</ul>
<div className='divider-dark'/>
<br></br>
<a
data-toggle='modal'

Просмотреть файл

@@ -9,7 +9,8 @@
"object-assign": "3.0.0",
"react-zeroclipboard-mixin": "0.1.0",
"twemoji": "1.4.1",
"babel-runtime": "5.8.24"
"babel-runtime": "5.8.24",
"marked": "0.3.5"
},
"devDependencies": {
"browserify": "11.0.1",

Просмотреть файл

@@ -8,16 +8,22 @@ var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var LOG_CHANGE_EVENT = 'log_change';
var CONFIG_CHANGE_EVENT = 'config_change';
class AdminStoreClass extends EventEmitter {
constructor() {
super();
this.logs = null;
this.config = null;
this.emitLogChange = this.emitLogChange.bind(this);
this.addLogChangeListener = this.addLogChangeListener.bind(this);
this.removeLogChangeListener = this.removeLogChangeListener.bind(this);
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
}
emitLogChange() {
@@ -32,6 +38,18 @@ class AdminStoreClass extends EventEmitter {
this.removeListener(LOG_CHANGE_EVENT, callback);
}
emitConfigChange() {
this.emit(CONFIG_CHANGE_EVENT);
}
addConfigChangeListener(callback) {
this.on(CONFIG_CHANGE_EVENT, callback);
}
removeConfigChangeListener(callback) {
this.removeListener(CONFIG_CHANGE_EVENT, callback);
}
getLogs() {
return this.logs;
}
@@ -39,6 +57,14 @@ class AdminStoreClass extends EventEmitter {
saveLogs(logs) {
this.logs = logs;
}
getConfig() {
return this.config;
}
saveConfig(config) {
this.config = config;
}
}
var AdminStore = new AdminStoreClass();
@@ -51,6 +77,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
AdminStore.saveLogs(action.logs);
AdminStore.emitLogChange();
break;
case ActionTypes.RECIEVED_CONFIG:
AdminStore.saveConfig(action.config);
AdminStore.emitConfigChange();
break;
default:
}
});

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше