From 6fb05fc82a0f146a706dda790ed5b17764b226ff Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Wed, 21 May 2014 20:06:14 +0200 Subject: [PATCH] Add Edit > Show ID with QR (fixes #243) --- Godeps/Godeps.json | 15 +- .../github.com/vitrun/qart/coding/coding.go | 815 ++++++++++++++++++ .../src/github.com/vitrun/qart/gf256/gf256.go | 241 ++++++ .../src/github.com/vitrun/qart/qr/png.go | 401 +++++++++ .../src/github.com/vitrun/qart/qr/qr.go | 109 +++ .../src/github.com/vitrun/qart/qr/resize.go | 151 ++++ auto/gui.files.go | 4 +- cmd/syncthing/gui.go | 13 + gui/app.js | 4 + gui/index.html | 46 +- 10 files changed, 1782 insertions(+), 17 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/vitrun/qart/coding/coding.go create mode 100644 Godeps/_workspace/src/github.com/vitrun/qart/gf256/gf256.go create mode 100644 Godeps/_workspace/src/github.com/vitrun/qart/qr/png.go create mode 100644 Godeps/_workspace/src/github.com/vitrun/qart/qr/qr.go create mode 100644 Godeps/_workspace/src/github.com/vitrun/qart/qr/resize.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 7d02d4ba2..80dac6605 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,10 +1,9 @@ { "ImportPath": "github.com/calmh/syncthing", - "GoVersion": "go1.2.1", + "GoVersion": "go1.2.2", "Packages": [ "./cmd/syncthing", "./cmd/assets", - "./cmd/stcli", "./discover/cmd/discosrv" ], "Deps": [ @@ -49,6 +48,18 @@ { "ImportPath": "github.com/juju/ratelimit", "Rev": "cbaa435c80a9716e086f25d409344b26c4039358" + }, + { + "ImportPath": "github.com/vitrun/qart/coding", + "Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0" + }, + { + "ImportPath": "github.com/vitrun/qart/gf256", + "Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0" + }, + { + "ImportPath": "github.com/vitrun/qart/qr", + "Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0" } ] } diff --git a/Godeps/_workspace/src/github.com/vitrun/qart/coding/coding.go b/Godeps/_workspace/src/github.com/vitrun/qart/coding/coding.go new file mode 100644 index 000000000..df7540944 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vitrun/qart/coding/coding.go @@ -0,0 +1,815 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package coding implements low-level QR coding details. +package coding + +import ( + "fmt" + "strconv" + "strings" + "github.com/vitrun/qart/gf256" +) + +// Field is the field for QR error correction. +var Field = gf256.NewField(0x11d, 2) + +// A Version represents a QR version. +// The version specifies the size of the QR code: +// a QR code with version v has 4v+17 pixels on a side. +// Versions number from 1 to 40: the larger the version, +// the more information the code can store. +type Version int + +const MinVersion = 1 +const MaxVersion = 40 + +func (v Version) String() string { + return strconv.Itoa(int(v)) +} + +func (v Version) sizeClass() int { + if v <= 9 { + return 0 + } + if v <= 26 { + return 1 + } + return 2 +} + +// DataBytes returns the number of data bytes that can be +// stored in a QR code with the given version and level. +func (v Version) DataBytes(l Level) int { + vt := &vtab[v] + lev := &vt.level[l] + return vt.bytes - lev.nblock*lev.check +} + +// Encoding implements a QR data encoding scheme. +// The implementations--Numeric, Alphanumeric, and String--specify +// the character set and the mapping from UTF-8 to code bits. +// The more restrictive the mode, the fewer code bits are needed. +type Encoding interface { + Check() error + Bits(v Version) int + Encode(b *Bits, v Version) +} + +type Bits struct { + b []byte + nbit int +} + +func (b *Bits) Reset() { + b.b = b.b[:0] + b.nbit = 0 +} + +func (b *Bits) Bits() int { + return b.nbit +} + +func (b *Bits) Bytes() []byte { + if b.nbit%8 != 0 { + panic("fractional byte") + } + return b.b +} + +func (b *Bits) Append(p []byte) { + if b.nbit%8 != 0 { + panic("fractional byte") + } + b.b = append(b.b, p...) + b.nbit += 8 * len(p) +} + +func (b *Bits) Write(v uint, nbit int) { + for nbit > 0 { + n := nbit + if n > 8 { + n = 8 + } + if b.nbit%8 == 0 { + b.b = append(b.b, 0) + } else { + m := -b.nbit & 7 + if n > m { + n = m + } + } + b.nbit += n + sh := uint(nbit - n) + b.b[len(b.b)-1] |= uint8(v >> sh << uint(-b.nbit&7)) + v -= v >> sh << sh + nbit -= n + } +} + +// Num is the encoding for numeric data. +// The only valid characters are the decimal digits 0 through 9. +type Num string + +func (s Num) String() string { + return fmt.Sprintf("Num(%#q)", string(s)) +} + +func (s Num) Check() error { + for _, c := range s { + if c < '0' || '9' < c { + return fmt.Errorf("non-numeric string %#q", string(s)) + } + } + return nil +} + +var numLen = [3]int{10, 12, 14} + +func (s Num) Bits(v Version) int { + return 4 + numLen[v.sizeClass()] + (10*len(s)+2)/3 +} + +func (s Num) Encode(b *Bits, v Version) { + b.Write((uint)(1), 4) + b.Write(uint(len(s)), numLen[v.sizeClass()]) + var i int + for i = 0; i+3 <= len(s); i += 3 { + w := uint(s[i]-'0')*100 + uint(s[i+1]-'0')*10 + uint(s[i+2]-'0') + b.Write(w, 10) + } + switch len(s) - i { + case 1: + w := uint(s[i] - '0') + b.Write(w, 4) + case 2: + w := uint(s[i]-'0')*10 + uint(s[i+1]-'0') + b.Write(w, 7) + } +} + +// Alpha is the encoding for alphanumeric data. +// The valid characters are 0-9A-Z$%*+-./: and space. +type Alpha string + +const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:" + +func (s Alpha) String() string { + return fmt.Sprintf("Alpha(%#q)", string(s)) +} + +func (s Alpha) Check() error { + for _, c := range s { + if strings.IndexRune(alphabet, c) < 0 { + return fmt.Errorf("non-alphanumeric string %#q", string(s)) + } + } + return nil +} + +var alphaLen = [3]int{9, 11, 13} + +func (s Alpha) Bits(v Version) int { + return 4 + alphaLen[v.sizeClass()] + (11*len(s)+1)/2 +} + +func (s Alpha) Encode(b *Bits, v Version) { + b.Write((uint)(2), 4) + b.Write(uint(len(s)), alphaLen[v.sizeClass()]) + var i int + for i = 0; i+2 <= len(s); i += 2 { + w := uint(strings.IndexRune(alphabet, rune(s[i])))*45 + + uint(strings.IndexRune(alphabet, rune(s[i+1]))) + b.Write(w, 11) + } + + if i < len(s) { + w := uint(strings.IndexRune(alphabet, rune(s[i]))) + b.Write(w, 6) + } +} + +// String is the encoding for 8-bit data. All bytes are valid. +type String string + +func (s String) String() string { + return fmt.Sprintf("String(%#q)", string(s)) +} + +func (s String) Check() error { + return nil +} + +var stringLen = [3]int{8, 16, 16} + +func (s String) Bits(v Version) int { + return 4 + stringLen[v.sizeClass()] + 8*len(s) +} + +func (s String) Encode(b *Bits, v Version) { + b.Write((uint)(4), 4) + b.Write(uint(len(s)), stringLen[v.sizeClass()]) + for i := 0; i < len(s); i++ { + b.Write(uint(s[i]), 8) + } +} + +// A Pixel describes a single pixel in a QR code. +type Pixel uint32 + +const ( + Black Pixel = 1 << iota + Invert +) + +func (p Pixel) Offset() uint { + return uint(p >> 6) +} + +func OffsetPixel(o uint) Pixel { + return Pixel(o << 6) +} + +func (r PixelRole) Pixel() Pixel { + return Pixel(r << 2) +} + +func (p Pixel) Role() PixelRole { + return PixelRole(p>>2) & 15 +} + +func (p Pixel) String() string { + s := p.Role().String() + if p&Black != 0 { + s += "+black" + } + if p&Invert != 0 { + s += "+invert" + } + s += "+" + strconv.FormatUint(uint64(p.Offset()), 10) + return s +} + +// A PixelRole describes the role of a QR pixel. +type PixelRole uint32 + +const ( + _ PixelRole = iota + Position // position squares (large) + Alignment // alignment squares (small) + Timing // timing strip between position squares + Format // format metadata + PVersion // version pattern + Unused // unused pixel + Data // data bit + Check // error correction check bit + Extra +) + +var roles = []string{ + "", + "position", + "alignment", + "timing", + "format", + "pversion", + "unused", + "data", + "check", + "extra", +} + +func (r PixelRole) String() string { + if Position <= r && r <= Check { + return roles[r] + } + return strconv.Itoa(int(r)) +} + +// A Level represents a QR error correction level. +// From least to most tolerant of errors, they are L, M, Q, H. +type Level int + +const ( + L Level = iota + M + Q + H +) + +func (l Level) String() string { + if L <= l && l <= H { + return "LMQH"[l : l+1] + } + return strconv.Itoa(int(l)) +} + +// A Code is a square pixel grid. +type Code struct { + Bitmap []byte // 1 is black, 0 is white + Size int // number of pixels on a side + Stride int // number of bytes per row +} + +func (c *Code) Black(x, y int) bool { + return 0 <= x && x < c.Size && 0 <= y && y < c.Size && + c.Bitmap[y*c.Stride+x/8]&(1<= pad { + break + } + b.Write((uint)(0x11), 8) + } + } +} + +func (b *Bits) AddCheckBytes(v Version, l Level) { + nd := v.DataBytes(l) + if b.nbit < nd*8 { + b.Pad(nd*8 - b.nbit) + } + if b.nbit != nd*8 { + panic("qr: too much data") + } + + dat := b.Bytes() + vt := &vtab[v] + lev := &vt.level[l] + db := nd / lev.nblock + extra := nd % lev.nblock + chk := make([]byte, lev.check) + rs := gf256.NewRSEncoder(Field, lev.check) + for i := 0; i < lev.nblock; i++ { + if i == lev.nblock-extra { + db++ + } + rs.ECC(dat[:db], chk) + b.Append(chk) + dat = dat[db:] + } + + if len(b.Bytes()) != vt.bytes { + panic("qr: internal error") + } +} + +func (p *Plan) Encode(text ...Encoding) (*Code, error) { + var b Bits + for _, t := range text { + if err := t.Check(); err != nil { + return nil, err + } + t.Encode(&b, p.Version) + } + if b.Bits() > p.DataBytes*8 { + return nil, fmt.Errorf("cannot encode %d bits into %d-bit code", b.Bits(), p.DataBytes*8) + } + b.AddCheckBytes(p.Version, p.Level) + bytes := b.Bytes() + + // Now we have the checksum bytes and the data bytes. + // Construct the actual code. + c := &Code{Size: len(p.Pixel), Stride: (len(p.Pixel) + 7) &^ 7} + c.Bitmap = make([]byte, c.Stride*c.Size) + crow := c.Bitmap + for _, row := range p.Pixel { + for x, pix := range row { + switch pix.Role() { + case Data, Check: + o := pix.Offset() + if bytes[o/8]&(1< 40 { + return nil, fmt.Errorf("invalid QR version %d", int(v)) + } + siz := 17 + int(v)*4 + m := grid(siz) + p.Pixel = m + + // Timing markers (overwritten by boxes). + const ti = 6 // timing is in row/column 6 (counting from 0) + for i := range m { + p := Timing.Pixel() + if i&1 == 0 { + p |= Black + } + m[i][ti] = p + m[ti][i] = p + } + + // Position boxes. + posBox(m, 0, 0) + posBox(m, siz-7, 0) + posBox(m, 0, siz-7) + + // Alignment boxes. + info := &vtab[v] + for x := 4; x+5 < siz; { + for y := 4; y+5 < siz; { + // don't overwrite timing markers + if (x < 7 && y < 7) || (x < 7 && y+5 >= siz-7) || (x+5 >= siz-7 && y < 7) { + } else { + alignBox(m, x, y) + } + if y == 4 { + y = info.apos + } else { + y += info.astride + } + } + if x == 4 { + x = info.apos + } else { + x += info.astride + } + } + + // Version pattern. + pat := vtab[v].pattern + if pat != 0 { + v := pat + for x := 0; x < 6; x++ { + for y := 0; y < 3; y++ { + p := PVersion.Pixel() + if v&1 != 0 { + p |= Black + } + m[siz-11+y][x] = p + m[x][siz-11+y] = p + v >>= 1 + } + } + } + + // One lonely black pixel + m[siz-8][8] = Unused.Pixel() | Black + + return p, nil +} + +// fplan adds the format pixels +func fplan(l Level, m Mask, p *Plan) error { + // Format pixels. + fb := uint32(l^1) << 13 // level: L=01, M=00, Q=11, H=10 + fb |= uint32(m) << 10 // mask + const formatPoly = 0x537 + rem := fb + for i := 14; i >= 10; i-- { + if rem&(1<>i)&1 == 1 { + pix |= Black + } + if (invert>>i)&1 == 1 { + pix ^= Invert | Black + } + // top left + switch { + case i < 6: + p.Pixel[i][8] = pix + case i < 8: + p.Pixel[i+1][8] = pix + case i < 9: + p.Pixel[8][7] = pix + default: + p.Pixel[8][14-i] = pix + } + // bottom right + switch { + case i < 8: + p.Pixel[8][siz-1-int(i)] = pix + default: + p.Pixel[siz-1-int(14-i)][8] = pix + } + } + return nil +} + +// lplan edits a version-only Plan to add information +// about the error correction levels. +func lplan(v Version, l Level, p *Plan) error { + p.Level = l + + nblock := vtab[v].level[l].nblock + ne := vtab[v].level[l].check + nde := (vtab[v].bytes - ne*nblock) / nblock + extra := (vtab[v].bytes - ne*nblock) % nblock + dataBits := (nde*nblock + extra) * 8 + checkBits := ne * nblock * 8 + + p.DataBytes = vtab[v].bytes - ne*nblock + p.CheckBytes = ne * nblock + p.Blocks = nblock + + // Make data + checksum pixels. + data := make([]Pixel, dataBits) + for i := range data { + data[i] = Data.Pixel() | OffsetPixel(uint(i)) + } + check := make([]Pixel, checkBits) + for i := range check { + check[i] = Check.Pixel() | OffsetPixel(uint(i+dataBits)) + } + + // Split into blocks. + dataList := make([][]Pixel, nblock) + checkList := make([][]Pixel, nblock) + for i := 0; i < nblock; i++ { + // The last few blocks have an extra data byte (8 pixels). + nd := nde + if i >= nblock-extra { + nd++ + } + dataList[i], data = data[0:nd*8], data[nd*8:] + checkList[i], check = check[0:ne*8], check[ne*8:] + } + if len(data) != 0 || len(check) != 0 { + panic("data/check math") + } + + // Build up bit sequence, taking first byte of each block, + // then second byte, and so on. Then checksums. + bits := make([]Pixel, dataBits+checkBits) + dst := bits + for i := 0; i < nde+1; i++ { + for _, b := range dataList { + if i*8 < len(b) { + copy(dst, b[i*8:(i+1)*8]) + dst = dst[8:] + } + } + } + for i := 0; i < ne; i++ { + for _, b := range checkList { + if i*8 < len(b) { + copy(dst, b[i*8:(i+1)*8]) + dst = dst[8:] + } + } + } + if len(dst) != 0 { + panic("dst math") + } + + // Sweep up pair of columns, + // then down, assigning to right then left pixel. + // Repeat. + // See Figure 2 of http://www.pclviewer.com/rs2/qrtopology.htm + siz := len(p.Pixel) + rem := make([]Pixel, 7) + for i := range rem { + rem[i] = Extra.Pixel() + } + src := append(bits, rem...) + for x := siz; x > 0; { + for y := siz - 1; y >= 0; y-- { + if p.Pixel[y][x-1].Role() == 0 { + p.Pixel[y][x-1], src = src[0], src[1:] + } + if p.Pixel[y][x-2].Role() == 0 { + p.Pixel[y][x-2], src = src[0], src[1:] + } + } + x -= 2 + if x == 7 { // vertical timing strip + x-- + } + for y := 0; y < siz; y++ { + if p.Pixel[y][x-1].Role() == 0 { + p.Pixel[y][x-1], src = src[0], src[1:] + } + if p.Pixel[y][x-2].Role() == 0 { + p.Pixel[y][x-2], src = src[0], src[1:] + } + } + x -= 2 + } + return nil +} + +// mplan edits a version+level-only Plan to add the mask. +func mplan(m Mask, p *Plan) error { + p.Mask = m + for y, row := range p.Pixel { + for x, pix := range row { + if r := pix.Role(); (r == Data || r == Check || r == Extra) && p.Mask.Invert(y, x) { + row[x] ^= Black | Invert + } + } + } + return nil +} + +// posBox draws a position (large) box at upper left x, y. +func posBox(m [][]Pixel, x, y int) { + pos := Position.Pixel() + // box + for dy := 0; dy < 7; dy++ { + for dx := 0; dx < 7; dx++ { + p := pos + if dx == 0 || dx == 6 || dy == 0 || dy == 6 || 2 <= dx && dx <= 4 && 2 <= dy && dy <= 4 { + p |= Black + } + m[y+dy][x+dx] = p + } + } + // white border + for dy := -1; dy < 8; dy++ { + if 0 <= y+dy && y+dy < len(m) { + if x > 0 { + m[y+dy][x-1] = pos + } + if x+7 < len(m) { + m[y+dy][x+7] = pos + } + } + } + for dx := -1; dx < 8; dx++ { + if 0 <= x+dx && x+dx < len(m) { + if y > 0 { + m[y-1][x+dx] = pos + } + if y+7 < len(m) { + m[y+7][x+dx] = pos + } + } + } +} + +// alignBox draw an alignment (small) box at upper left x, y. +func alignBox(m [][]Pixel, x, y int) { + // box + align := Alignment.Pixel() + for dy := 0; dy < 5; dy++ { + for dx := 0; dx < 5; dx++ { + p := align + if dx == 0 || dx == 4 || dy == 0 || dy == 4 || dx == 2 && dy == 2 { + p |= Black + } + m[y+dy][x+dx] = p + } + } +} + diff --git a/Godeps/_workspace/src/github.com/vitrun/qart/gf256/gf256.go b/Godeps/_workspace/src/github.com/vitrun/qart/gf256/gf256.go new file mode 100644 index 000000000..ada96619e --- /dev/null +++ b/Godeps/_workspace/src/github.com/vitrun/qart/gf256/gf256.go @@ -0,0 +1,241 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package gf256 implements arithmetic over the Galois Field GF(256). +package gf256 + +import "strconv" + +// A Field represents an instance of GF(256) defined by a specific polynomial. +type Field struct { + log [256]byte // log[0] is unused + exp [510]byte +} + +// NewField returns a new field corresponding to the polynomial poly +// and generator α. The Reed-Solomon encoding in QR codes uses +// polynomial 0x11d with generator 2. +// +// The choice of generator α only affects the Exp and Log operations. +func NewField(poly, α int) *Field { + if poly < 0x100 || poly >= 0x200 || reducible(poly) { + panic("gf256: invalid polynomial: " + strconv.Itoa(poly)) + } + + var f Field + x := 1 + for i := 0; i < 255; i++ { + if x == 1 && i != 0 { + panic("gf256: invalid generator " + strconv.Itoa(α) + + " for polynomial " + strconv.Itoa(poly)) + } + f.exp[i] = byte(x) + f.exp[i+255] = byte(x) + f.log[x] = byte(i) + x = mul(x, α, poly) + } + f.log[0] = 255 + for i := 0; i < 255; i++ { + if f.log[f.exp[i]] != byte(i) { + panic("bad log") + } + if f.log[f.exp[i+255]] != byte(i) { + panic("bad log") + } + } + for i := 1; i < 256; i++ { + if f.exp[f.log[i]] != byte(i) { + panic("bad log") + } + } + + return &f +} + +// nbit returns the number of significant in p. +func nbit(p int) uint { + n := uint(0) + for ; p > 0; p >>= 1 { + n++ + } + return n +} + +// polyDiv divides the polynomial p by q and returns the remainder. +func polyDiv(p, q int) int { + np := nbit(p) + nq := nbit(q) + for ; np >= nq; np-- { + if p&(1<<(np-1)) != 0 { + p ^= q << (np - nq) + } + } + return p +} + +// mul returns the product x*y mod poly, a GF(256) multiplication. +func mul(x, y, poly int) int { + z := 0 + for x > 0 { + if x&1 != 0 { + z ^= y + } + x >>= 1 + y <<= 1 + if y&0x100 != 0 { + y ^= poly + } + } + return z +} + +// reducible reports whether p is reducible. +func reducible(p int) bool { + // Multiplying n-bit * n-bit produces (2n-1)-bit, + // so if p is reducible, one of its factors must be + // of np/2+1 bits or fewer. + np := nbit(p) + for q := 2; q < int(1<<(np/2+1)); q++ { + if polyDiv(p, q) == 0 { + return true + } + } + return false +} + +// Add returns the sum of x and y in the field. +func (f *Field) Add(x, y byte) byte { + return x ^ y +} + +// Exp returns the the base-α exponential of e in the field. +// If e < 0, Exp returns 0. +func (f *Field) Exp(e int) byte { + if e < 0 { + return 0 + } + return f.exp[e%255] +} + +// Log returns the base-α logarithm of x in the field. +// If x == 0, Log returns -1. +func (f *Field) Log(x byte) int { + if x == 0 { + return -1 + } + return int(f.log[x]) +} + +// Inv returns the multiplicative inverse of x in the field. +// If x == 0, Inv returns 0. +func (f *Field) Inv(x byte) byte { + if x == 0 { + return 0 + } + return f.exp[255-f.log[x]] +} + +// Mul returns the product of x and y in the field. +func (f *Field) Mul(x, y byte) byte { + if x == 0 || y == 0 { + return 0 + } + return f.exp[int(f.log[x])+int(f.log[y])] +} + +// An RSEncoder implements Reed-Solomon encoding +// over a given field using a given number of error correction bytes. +type RSEncoder struct { + f *Field + c int + gen []byte + lgen []byte + p []byte +} + +func (f *Field) gen(e int) (gen, lgen []byte) { + // p = 1 + p := make([]byte, e+1) + p[e] = 1 + + for i := 0; i < e; i++ { + // p *= (x + Exp(i)) + // p[j] = p[j]*Exp(i) + p[j+1]. + c := f.Exp(i) + for j := 0; j < e; j++ { + p[j] = f.Mul(p[j], c) ^ p[j+1] + } + p[e] = f.Mul(p[e], c) + } + + // lp = log p. + lp := make([]byte, e+1) + for i, c := range p { + if c == 0 { + lp[i] = 255 + } else { + lp[i] = byte(f.Log(c)) + } + } + + return p, lp +} + +// NewRSEncoder returns a new Reed-Solomon encoder +// over the given field and number of error correction bytes. +func NewRSEncoder(f *Field, c int) *RSEncoder { + gen, lgen := f.gen(c) + return &RSEncoder{f: f, c: c, gen: gen, lgen: lgen} +} + +// ECC writes to check the error correcting code bytes +// for data using the given Reed-Solomon parameters. +func (rs *RSEncoder) ECC(data []byte, check []byte) { + if len(check) < rs.c { + panic("gf256: invalid check byte length") + } + if rs.c == 0 { + return + } + + // The check bytes are the remainder after dividing + // data padded with c zeros by the generator polynomial. + + // p = data padded with c zeros. + var p []byte + n := len(data) + rs.c + if len(rs.p) >= n { + p = rs.p + } else { + p = make([]byte, n) + } + copy(p, data) + for i := len(data); i < len(p); i++ { + p[i] = 0 + } + + // Divide p by gen, leaving the remainder in p[len(data):]. + // p[0] is the most significant term in p, and + // gen[0] is the most significant term in the generator, + // which is always 1. + // To avoid repeated work, we store various values as + // lv, not v, where lv = log[v]. + f := rs.f + lgen := rs.lgen[1:] + for i := 0; i < len(data); i++ { + c := p[i] + if c == 0 { + continue + } + q := p[i+1:] + exp := f.exp[f.log[c]:] + for j, lg := range lgen { + if lg != 255 { // lgen uses 255 for log 0 + q[j] ^= exp[lg] + } + } + } + copy(check, p[len(data):]) + rs.p = p +} diff --git a/Godeps/_workspace/src/github.com/vitrun/qart/qr/png.go b/Godeps/_workspace/src/github.com/vitrun/qart/qr/png.go new file mode 100644 index 000000000..9a45f0771 --- /dev/null +++ b/Godeps/_workspace/src/github.com/vitrun/qart/qr/png.go @@ -0,0 +1,401 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package qr + +// PNG writer for QR codes. + +import ( + "bytes" + "encoding/binary" + "hash" + "hash/crc32" +) + +// PNG returns a PNG image displaying the code. +// +// PNG uses a custom encoder tailored to QR codes. +// Its compressed size is about 2x away from optimal, +// but it runs about 20x faster than calling png.Encode +// on c.Image(). +func (c *Code) PNG() []byte { + var p pngWriter + return p.encode(c) +} + +type pngWriter struct { + tmp [16]byte + wctmp [4]byte + buf bytes.Buffer + zlib bitWriter + crc hash.Hash32 +} + +var pngHeader = []byte("\x89PNG\r\n\x1a\n") + +func (w *pngWriter) encode(c *Code) []byte { + scale := c.Scale + siz := c.Size + + w.buf.Reset() + + // Header + w.buf.Write(pngHeader) + + // Header block + binary.BigEndian.PutUint32(w.tmp[0:4], uint32((siz+8)*scale)) + binary.BigEndian.PutUint32(w.tmp[4:8], uint32((siz+8)*scale)) + w.tmp[8] = 1 // 1-bit + w.tmp[9] = 0 // gray + w.tmp[10] = 0 + w.tmp[11] = 0 + w.tmp[12] = 0 + w.writeChunk("IHDR", w.tmp[:13]) + + // Comment + w.writeChunk("tEXt", comment) + + // Data + w.zlib.writeCode(c) + w.writeChunk("IDAT", w.zlib.bytes.Bytes()) + + // End + w.writeChunk("IEND", nil) + + return w.buf.Bytes() +} + +var comment = []byte("Software\x00QR-PNG http://qr.swtch.com/") + +func (w *pngWriter) writeChunk(name string, data []byte) { + if w.crc == nil { + w.crc = crc32.NewIEEE() + } + binary.BigEndian.PutUint32(w.wctmp[0:4], uint32(len(data))) + w.buf.Write(w.wctmp[0:4]) + w.crc.Reset() + copy(w.wctmp[0:4], name) + w.buf.Write(w.wctmp[0:4]) + w.crc.Write(w.wctmp[0:4]) + w.buf.Write(data) + w.crc.Write(data) + crc := w.crc.Sum32() + binary.BigEndian.PutUint32(w.wctmp[0:4], crc) + w.buf.Write(w.wctmp[0:4]) +} + +func (b *bitWriter) writeCode(c *Code) { + const ftNone = 0 + + b.adler32.Reset() + b.bytes.Reset() + b.nbit = 0 + + scale := c.Scale + siz := c.Size + + // zlib header + b.tmp[0] = 0x78 + b.tmp[1] = 0 + b.tmp[1] += uint8(31 - (uint16(b.tmp[0])<<8+uint16(b.tmp[1]))%31) + b.bytes.Write(b.tmp[0:2]) + + // Start flate block. + b.writeBits(1, 1, false) // final block + b.writeBits(1, 2, false) // compressed, fixed Huffman tables + + // White border. + // First row. + b.byte(ftNone) + n := (scale*(siz+8) + 7) / 8 + b.byte(255) + b.repeat(n-1, 1) + // 4*scale rows total. + b.repeat((4*scale-1)*(1+n), 1+n) + + for i := 0; i < 4*scale; i++ { + b.adler32.WriteNByte(ftNone, 1) + b.adler32.WriteNByte(255, n) + } + + row := make([]byte, 1+n) + for y := 0; y < siz; y++ { + row[0] = ftNone + j := 1 + var z uint8 + nz := 0 + for x := -4; x < siz+4; x++ { + // Raw data. + for i := 0; i < scale; i++ { + z <<= 1 + if !c.Black(x, y) { + z |= 1 + } + if nz++; nz == 8 { + row[j] = z + j++ + nz = 0 + } + } + } + if j < len(row) { + row[j] = z + } + for _, z := range row { + b.byte(z) + } + + // Scale-1 copies. + b.repeat((scale-1)*(1+n), 1+n) + + b.adler32.WriteN(row, scale) + } + + // White border. + // First row. + b.byte(ftNone) + b.byte(255) + b.repeat(n-1, 1) + // 4*scale rows total. + b.repeat((4*scale-1)*(1+n), 1+n) + + for i := 0; i < 4*scale; i++ { + b.adler32.WriteNByte(ftNone, 1) + b.adler32.WriteNByte(255, n) + } + + // End of block. + b.hcode(256) + b.flushBits() + + // adler32 + binary.BigEndian.PutUint32(b.tmp[0:], b.adler32.Sum32()) + b.bytes.Write(b.tmp[0:4]) +} + +// A bitWriter is a write buffer for bit-oriented data like deflate. +type bitWriter struct { + bytes bytes.Buffer + bit uint32 + nbit uint + + tmp [4]byte + adler32 adigest +} + +func (b *bitWriter) writeBits(bit uint32, nbit uint, rev bool) { + // reverse, for huffman codes + if rev { + br := uint32(0) + for i := uint(0); i < nbit; i++ { + br |= ((bit >> i) & 1) << (nbit - 1 - i) + } + bit = br + } + b.bit |= bit << b.nbit + b.nbit += nbit + for b.nbit >= 8 { + b.bytes.WriteByte(byte(b.bit)) + b.bit >>= 8 + b.nbit -= 8 + } +} + +func (b *bitWriter) flushBits() { + if b.nbit > 0 { + b.bytes.WriteByte(byte(b.bit)) + b.nbit = 0 + b.bit = 0 + } +} + +func (b *bitWriter) hcode(v int) { + /* + Lit Value Bits Codes + --------- ---- ----- + 0 - 143 8 00110000 through + 10111111 + 144 - 255 9 110010000 through + 111111111 + 256 - 279 7 0000000 through + 0010111 + 280 - 287 8 11000000 through + 11000111 + */ + switch { + case v <= 143: + b.writeBits(uint32(v)+0x30, 8, true) + case v <= 255: + b.writeBits(uint32(v-144)+0x190, 9, true) + case v <= 279: + b.writeBits(uint32(v-256)+0, 7, true) + case v <= 287: + b.writeBits(uint32(v-280)+0xc0, 8, true) + default: + panic("invalid hcode") + } +} + +func (b *bitWriter) byte(x byte) { + b.hcode(int(x)) +} + +func (b *bitWriter) codex(c int, val int, nx uint) { + b.hcode(c + val>>nx) + b.writeBits(uint32(val)&(1<= 258+3; n -= 258 { + b.repeat1(258, d) + } + if n > 258 { + // 258 < n < 258+3 + b.repeat1(10, d) + b.repeat1(n-10, d) + return + } + if n < 3 { + panic("invalid flate repeat") + } + b.repeat1(n, d) +} + +func (b *bitWriter) repeat1(n, d int) { + /* + Extra Extra Extra + Code Bits Length(s) Code Bits Lengths Code Bits Length(s) + ---- ---- ------ ---- ---- ------- ---- ---- ------- + 257 0 3 267 1 15,16 277 4 67-82 + 258 0 4 268 1 17,18 278 4 83-98 + 259 0 5 269 2 19-22 279 4 99-114 + 260 0 6 270 2 23-26 280 4 115-130 + 261 0 7 271 2 27-30 281 5 131-162 + 262 0 8 272 2 31-34 282 5 163-194 + 263 0 9 273 3 35-42 283 5 195-226 + 264 0 10 274 3 43-50 284 5 227-257 + 265 1 11,12 275 3 51-58 285 0 258 + 266 1 13,14 276 3 59-66 + */ + switch { + case n <= 10: + b.codex(257, n-3, 0) + case n <= 18: + b.codex(265, n-11, 1) + case n <= 34: + b.codex(269, n-19, 2) + case n <= 66: + b.codex(273, n-35, 3) + case n <= 130: + b.codex(277, n-67, 4) + case n <= 257: + b.codex(281, n-131, 5) + case n == 258: + b.hcode(285) + default: + panic("invalid repeat length") + } + + /* + Extra Extra Extra + Code Bits Dist Code Bits Dist Code Bits Distance + ---- ---- ---- ---- ---- ------ ---- ---- -------- + 0 0 1 10 4 33-48 20 9 1025-1536 + 1 0 2 11 4 49-64 21 9 1537-2048 + 2 0 3 12 5 65-96 22 10 2049-3072 + 3 0 4 13 5 97-128 23 10 3073-4096 + 4 1 5,6 14 6 129-192 24 11 4097-6144 + 5 1 7,8 15 6 193-256 25 11 6145-8192 + 6 2 9-12 16 7 257-384 26 12 8193-12288 + 7 2 13-16 17 7 385-512 27 12 12289-16384 + 8 3 17-24 18 8 513-768 28 13 16385-24576 + 9 3 25-32 19 8 769-1024 29 13 24577-32768 + */ + if d <= 4 { + b.writeBits(uint32(d-1), 5, true) + } else if d <= 32768 { + nbit := uint(16) + for d <= 1<<(nbit-1) { + nbit-- + } + v := uint32(d - 1) + v &^= 1 << (nbit - 1) // top bit is implicit + code := uint32(2*nbit - 2) // second bit is low bit of code + code |= v >> (nbit - 2) + v &^= 1 << (nbit - 2) + b.writeBits(code, 5, true) + // rest of bits follow + b.writeBits(uint32(v), nbit-2, false) + } else { + panic("invalid repeat distance") + } +} + +func (b *bitWriter) run(v byte, n int) { + if n == 0 { + return + } + b.byte(v) + if n-1 < 3 { + for i := 0; i < n-1; i++ { + b.byte(v) + } + } else { + b.repeat(n-1, 1) + } +} + +type adigest struct { + a, b uint32 +} + +func (d *adigest) Reset() { d.a, d.b = 1, 0 } + +const amod = 65521 + +func aupdate(a, b uint32, pi byte, n int) (aa, bb uint32) { + // TODO(rsc): 6g doesn't do magic multiplies for b %= amod, + // only for b = b%amod. + + // invariant: a, b < amod + if pi == 0 { + b += uint32(n%amod) * a + b = b % amod + return a, b + } + + // n times: + // a += pi + // b += a + // is same as + // b += n*a + n*(n+1)/2*pi + // a += n*pi + m := uint32(n) + b += (m % amod) * a + b = b % amod + b += (m * (m + 1) / 2) % amod * uint32(pi) + b = b % amod + a += (m % amod) * uint32(pi) + a = a % amod + return a, b +} + +func afinish(a, b uint32) uint32 { + return b<<16 | a +} + +func (d *adigest) WriteN(p []byte, n int) { + for i := 0; i < n; i++ { + for _, pi := range p { + d.a, d.b = aupdate(d.a, d.b, pi, 1) + } + } +} + +func (d *adigest) WriteNByte(pi byte, n int) { + d.a, d.b = aupdate(d.a, d.b, pi, n) +} + +func (d *adigest) Sum32() uint32 { return afinish(d.a, d.b) } + diff --git a/Godeps/_workspace/src/github.com/vitrun/qart/qr/qr.go b/Godeps/_workspace/src/github.com/vitrun/qart/qr/qr.go new file mode 100644 index 000000000..5660b4c6d --- /dev/null +++ b/Godeps/_workspace/src/github.com/vitrun/qart/qr/qr.go @@ -0,0 +1,109 @@ +package qr + +import ( + "errors" + "image" + "image/color" + "github.com/vitrun/qart/coding" +) + +// A Level denotes a QR error correction level. +// From least to most tolerant of errors, they are L, M, Q, H. +type Level int + +const ( + L Level = iota // 20% redundant + M // 38% redundant + Q // 55% redundant + H // 65% redundant +) + +// Encode returns an encoding of text at the given error correction level. +func Encode(text string, level Level) (*Code, error) { + // Pick data encoding, smallest first. + // We could split the string and use different encodings + // but that seems like overkill for now. + var enc coding.Encoding + switch { + case coding.Num(text).Check() == nil: + enc = coding.Num(text) + case coding.Alpha(text).Check() == nil: + enc = coding.Alpha(text) + default: + enc = coding.String(text) + } + + // Pick size. + l := coding.Level(level) + var v coding.Version + for v = coding.MinVersion; ; v++ { + if v > coding.MaxVersion { + return nil, errors.New("text too long to encode as QR") + } + if enc.Bits(v) <= v.DataBytes(l)*8 { + break + } + } + + // Build and execute plan. + p, err := coding.NewPlan(v, l, 0) + if err != nil { + return nil, err + } + cc, err := p.Encode(enc) + if err != nil { + return nil, err + } + + // TODO: Pick appropriate mask. + + return &Code{cc.Bitmap, cc.Size, cc.Stride, 8}, nil +} + +// A Code is a square pixel grid. +// It implements image.Image and direct PNG encoding. +type Code struct { + Bitmap []byte // 1 is black, 0 is white + Size int // number of pixels on a side + Stride int // number of bytes per row + Scale int // number of image pixels per QR pixel +} + +// Black returns true if the pixel at (x,y) is black. +func (c *Code) Black(x, y int) bool { + return 0 <= x && x < c.Size && 0 <= y && y < c.Size && + c.Bitmap[y*c.Stride+x/8]&(1< 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + qxy := qx * qy + sum[index+0] += r64 * qxy + sum[index+1] += g64 * qxy + sum[index+2] += b64 * qxy + sum[index+3] += a64 * qxy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, (uint64)(n)) +} + + +// ResizeNRGBA returns a scaled copy of the RGBA image slice r of m. +// The returned image has width w and height h. +func ResizeNRGBA(m *image.NRGBA, r image.Rectangle, w, h int) *image.RGBA { + ww, hh := uint64(w), uint64(h) + dx, dy := uint64(r.Dx()), uint64(r.Dy()) + // See comment in Resize. + n, sum := dx*dy, make([]uint64, 4*w*h) + for y := r.Min.Y; y < r.Max.Y; y++ { + pix := m.Pix[(y-r.Min.Y)*m.Stride:] + for x := r.Min.X; x < r.Max.X; x++ { + // Get the source pixel. + p := pix[(x-r.Min.X)*4:] + r64 := uint64(p[0]) + g64 := uint64(p[1]) + b64 := uint64(p[2]) + a64 := uint64(p[3]) + r64 = (r64 * a64) / 255 + g64 = (g64 * a64) / 255 + b64 = (b64 * a64) / 255 + // Spread the source pixel over 1 or more destination rows. + py := uint64(y) * hh + for remy := hh; remy > 0; { + qy := dy - (py % dy) + if qy > remy { + qy = remy + } + // Spread the source pixel over 1 or more destination columns. + px := uint64(x) * ww + index := 4 * ((py/dy)*ww + (px / dx)) + for remx := ww; remx > 0; { + qx := dx - (px % dx) + if qx > remx { + qx = remx + } + qxy := qx * qy + sum[index+0] += r64 * qxy + sum[index+1] += g64 * qxy + sum[index+2] += b64 * qxy + sum[index+3] += a64 * qxy + index += 4 + px += qx + remx -= qx + } + py += qy + remy -= qy + } + } + } + return average(sum, w, h, (uint64)(n)) +} + +// Resample returns a resampled copy of the image slice r of m. +// The returned image has width w and height h. +func Resample(m image.Image, r image.Rectangle, w, h int) *image.RGBA { + if w < 0 || h < 0 { + return nil + } + if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 { + return image.NewRGBA(image.Rect(0, 0, w, h)) + } + curw, curh := r.Dx(), r.Dy() + img := image.NewRGBA(image.Rect(0, 0, w, h)) + for y := 0; y < h; y++ { + for x := 0; x < w; x++ { + // Get a source pixel. + subx := x * curw / w + suby := y * curh / h + r32, g32, b32, a32 := m.At(subx, suby).RGBA() + r := uint8(r32 >> 8) + g := uint8(g32 >> 8) + b := uint8(b32 >> 8) + a := uint8(a32 >> 8) + img.SetRGBA(x, y, color.RGBA{r, g, b, a}) + } +} +return img +} + diff --git a/auto/gui.files.go b/auto/gui.files.go index 4da22b61b..efb6f4777 100644 --- a/auto/gui.files.go +++ b/auto/gui.files.go @@ -18,7 +18,7 @@ func init() { bs, _ = ioutil.ReadAll(gr) Assets["angular.min.js"] = bs - bs, _ = hex.DecodeString("1f8b080000096e8800ffd41c5d73db36f23dbf0255d3924a64cae9dd746eac389dd6497bbe7c79e2242fae1f2011921853a00a82b635aefefb2d3e48020448d176d2a69eb98b0d2cf61bbb8b05d8f1a34f799a508ea62cbbca093b409c1564846619e5092d48f9f73a2d72f13ff5377a347e307eb448b3294ed1c30334c7690e40982e8a1433fdb7007a1014f04bce5932e3c1e4c1834bcc50bea133be4ce8021d962ba25516172909836a2e18a1b3f3e1442e28583ac580e610058ce4124f051709465996a68485c169397ac4590a18e605fc9d6414850ff359b6060e1f2e395f0fd1cd03043f02f79a91cbe7980be4fb936a7441f8db97302464ad470571ccb8e25c8a089c8849855db04289249803c0cd76d2989c270b777cb5397e2e240bac519ac54420393bf72039a642508b3d3d4f18cb98675d4e087d21e65c4aa07992ba6c31b2ce4a29e4f8788c4ec18c7491a32999678ca06996a5394ab3ec02463827cca6c8b904065ee4f84d121fa0e05592734201115807249c81af48aba11396f16c96a54841a09fe318d49d931c00f9660d7e187072cde12f6d05e588db9181fc35be3e25347e395de706fab7055f64c266ef84995f25ab84a3f065f2cb381fd6b869b19a12d689fd1de0c3f49882a097383d3528a819544ea1f0d698b5e7f891ebc9bbe33fc10cc30649df913f0a003295032a43a020584963a9a31aa43f7e4072b4849d4c1a9a17c87f4d5282d4ac32407fd51b147e9391e6674a5f503c4d496c10515308e632d8eda4c62bdcb393ed57d9ac05a79cb90bca5331fe8b8aa4a6878b61548f9be8acf51f4ee889cb8e1a4162d2bff8dc8e438b2239f56f3fbdab4cf57d38becf96fb600b2ab0fd5cf02581e431c332f47eb044de89f004e7f955c6e26ea4069446bcae473ab97dffcadc5c3080fefbfefdc92982888680502f5b97eaaed20ba48bd3623623242671586617f193cc51f88d4c26e6a861aa84263c1c4eeca930f896120ed25cc8981d0c4590c669182c9398040d683755899fadc5429db86ec946bdb09b095f66b459d9ba2afb15436c70f5d5ce2c23bc60b44d48bf9adb747933c5b38b98656b3035d003cf02635f90cd34c32cd605ccb645d55d0256e9730e722c0570558398bcc93a2402846159dc3c46c138dfc0465c018fb9f0a63c0febc531e6b8299ced76139f6115466043ac37581eaa5221f473a791970632d7d5bfbf9d7e82ac1481cef2d02c1a8611eca51778b6349027b16318af026431f293407318c000a13318f8f0eef8285bad330a014060eaab1e430d12ef59129f3b9a684a65feeee7d1a8f27a5b4a548f34bb02eaa2d684eaee2a1c8e2c081ec3642860f6aa9a7488c6e8c9fefebe0d99c43afc943f46090bebbd6e9050c8cc7589db98cd0a5e4d5bf3222882c65142519b8265841393d112e76faf28d4716bc2f8461aca032f7ecab3c5c499dd3a239c6d5ab008a2c2a49570af315f462b7c1dee8fd07fd023650909714c7fd97092bfcf38a4f53d4fb9ee4009d5f378e8726851ae14d74e1a8aab3eb42db056e25b04f96fb644216953ada394fdbe2278005d6b58eef4f8b041cec5603b98b9400d35f6a1cf39ed4355338ceddaacea40d47b9f368f517e72dbc983e651092a3c5ee456b817c3cddc266a8b6c6e4725010771e9104e66058dc93ca150fcf9531f142cf402f6380dccec63517051836de0d000bbf81b41a215f329cfd66ba0ecc52c8fed5cc5984044660f19395faf2ec1e5bf67fbe711cf3e00017604a68164f3584d805da63967e193e1c496432f3f4403713c84626080fefc13d5a3c7714a064d59d434b8d90085839a4bd12c38216c06f9032f8836cc6334f86e38f04aab556208d44ceeebec288582f3cbdb3ba1f3ec0b183b16e731f6d96c6d192c48c034ad94f53e0cdaaab81a4daeecde8a69cd9215661b2f268ffa6c13da2ef1c5ed0859bc4d608f76555f4de603897fbf1f56cb82eb1907a90002b291d74dc4b65224c63e0083874953ad32dbcdd30cea4720e30f8aa281e5098a62f8686e55f6825b11e401ce931cf582e80dfc7bfcfcdcf63901d8d44d391e899a312592ac5022a8c297344b4f39a6486824686424444423b37d990e4e28141ba5491612d077c326c68e80133c4f722d7b15883d6a3d9e0991fe094acd2e6eab4f7d6eba8dd256504ae6edda7203f557ab2e3736f6d2991b0877eacc8c8baeca4423e82fd698664c2a4df7a1bae2fa4f1df63654feb789d008029d39aa5d948f64a719043336a7d2d18cbb85163635c4256139e0f6f1f725d5932690783fb6d3de6167c8b5b120ec68c7165748b0120726799562deac00869413b3fd21f82dc9d25a9548619d58470d21a2c61ba5842ef812ca2df4a445e2aa38e81054638332b9d519dee0952b6f9737007c0b4726480753b689cb4a1d4eb83ffa533e5f26f91b1fa74d9b5065bac6b2b0a1615d7329e05e55d6202cd4f1482eb28bfba68a76e9a78f727a6b86c409373af22d2dc130f8b6bc35ebdba6944de9a1b7c0c597643749efad62a3bd6901456fd772ff47eace40876a2b44b850a79c45f93a4d78188c8464786d6cbc6b63e35d471c72193882e764bfce72a70f07a4401dff3b7dfb26cae5dd6432df841627c311ba59121c43a8394037c15146811fbef71e7c2b0095e235b0a5ee14c69ff28c065b8bb0cf2203d1ff1eb4750264ebba55df569bdc6ed8b774db6fdba66ed195c66cb6ed77dd283744cb97058fc5d6ba8b6c7ea64a94deee8caf916f2cb89b76b677975f6c616fca6984e0126dc118389a5ef13022d7e0777178b31d9561d565459000f5bdb8864de355a305764ad2b96a1937b2bf95fc5d796bc6a2eac20f36283aace27d351c7dca120a7b1679fc4600bf004e32163d843d72c224d3561017262bd576a780665b00c76ecedfa9f91b534620196f285e01cd6d0fedfbe3a0ad7e3fcc5faf1c38b8134ebaf5e3a3e8dce6c97ebe5f25bbafe37c7ae8577a79d370e9d3dfd44e6dbaaf9aedb1bb2564930fa3df28af394461a2ae3ad42d96bf332ce76403bb81b331d143ca7b48da94d6d5fc8e94fe7764d466428102a5d35fcb4a1142d208c5192523944c6e2b682f9fd754904ff70e5415689bf53178408a67241ca331301c00b96a64af1cd1e58d83b38a52be307cc7f2a9222274e7aa46ddedc98b1f94a0a7f61e55a71a9878fcd8d7db3061cf9273f3e4692ba5e34ab65c5c4b3c714035e7761a2c7fa68ce08b1e3d0f754309985ab6b492785de4cbd04dcd2d312dca33c6c3b2dd8019b96be8f91a7662c69784957cb6157766d3a0358ef70ee3a236710377cb79b233422450503543f91dbc9a364c048e3971dc9ef66cb198c2b73ba847629ca6dd762863a2e151b5f59a81453bb573cc36c08cc36cee2f7bc54da838c5f5740d7573eafa46db519b44ef931541cf9c87aa3bbd639612cc5e9417b59d25a1f9fad5e2f3cce65af772f6d09373c9d6ce8429d78d2527819fcb394ba0ee4f37ae5961539bacd635c8ed9d5776eaccfce577e15c56f9f0ff55629a55817ad46c3589b9e1b0a30102785acec0ebac8fc39470f6db215f2b4324f077307bbba3975e511fbd722891c5c58a340504cc6dd759cc40521674cea326a7a2f3acb5889e9501e4dc7daf77e7e3a0d05bcfb386e0e8f31cc41c73ecb48347fd773e82fdf5228b62b55366f579c03afb1cc5aac5b353ac6a2a4eb12a56395055f97176eeced9be69e433d73dabe8a4dae1e529c9c1e22b16fde468b9110e152d7f0e358450c9ec466da003cd48f3856633bf8a1f7534f6f3eb9a489de34a60a50bfd575b8927349f0b6f4ca4aadb02db5755f329a5746fe35eeef879fa05da46960d3cd1d4ba68baa711fe29e764f134dbb0518f57c4fa4eefb68fdef432efabb71d146ff96eb97a83e37d9b6c40e86fa3e4f352f7c8b083296da55b32557da6d5ca54db15c78e4b107d9cb77aaaadc75107d79bba42f7c1fbcea3aded2bbd3d5ee375d8be875a11c8f719cd37e1bd4c31168faeee668f6a834a5730c7769e13cc4f1ba058283fa20a6d8146f2a1f53ec049712ad60c95867884a62597c60525965785eac15d7d4589beff1e6980a917a0796fabd13cd5e043df6969ef897374d3cb9ee96565fd6c6055195be2757a320dbc6a5d8d592d7d562d05ed18ba9169b14537387a9e30c8b319db48d2d55ffda9d7089e99085c1e8423a7e6379d2be373c6d453b85b07afd5192bd3bc66433b4b7935ef1294896565524c8d12abd14d5e99c45255c5ac44a37868089daa0d6c68d46622b59988c92c5941aa0232291c620a8b973859243c17df38cdcafa4658447c38e83c26d4e8f7ad63974250be6a57effce4af69b650bfe0a9243d140fd5ab9927fbe57951506e3c8a071ee15cad30dba22936b7e627bdba7b10500c10d8fe90b7e45eaf363e31a1eb828f907cfdea11514e473cfb35b926715829d05a55071019045c86a609150fadfaf3d3dce472509aa10a046dcf5bf751eb1b5985e51984ac1ffe8d1e99ff38f5b7841c1f7a40273eaadd5afa61289e4d07e8b7e436acf5e1e91eccbceec74c2717f720ffd24fde7c29cb323076e90f624db0cbcf56447c30ff15f9997c466cfcd3a1cd26e87dfcec369cf561e93e6ed68b974e26ee41fde20b3859be849473471fb398f73e7f6a218ad32bbcc9df949f7c7f79fff6bebb37d9dfc5f16c59d08be3e79f99591fab03e3c19a7cc28f994cc34acbf28960388e6e9e8c7edc8e17cd23b804de89560f49607d12d9ab3ad79d8e72024ef6b7eb40dd828ecf7e1ffffefbf9d8ab02f19590124f77f39f1ea27fb5f886617faf7f0ca2281a8bef8814c21c0eec243491effd30545a1c8c076d5a8c65e19a5c9230c8e48150752ebb7469ee33f59f4e3940c15130328665ff5effb759aa61ce30cd6769113b33f2ac73d0fc5a4a3d6d03dc87fa5783c4d6404b56408e8b8fe29f2a3b8a83cde1407ccb3f4074b127bf1b391cd8c7de338d33825277f0ece958ae7ca629b8dbcd505341933f0ad99b3294d4a5a33f0a582cfe4b128bd78213438a34a11707350ea9861122e96a8430e70caae4196769d33dc458f4100c9d13964705cd97c99c57e787f03221571f71eaef59ca6f98fa74c3ca9ff1185d11047bac48631a70558a82b6bdc08a33d0ec470195f04d435bb2933a7196ea4f086ade5487ad12e4bc8339be54bd5344842039c2292338dedc893dd95e6be76f370f498e14c2cfaa1d6744fb58a59f4693d9b916d3eefc7f000000ffff010000ffff0ac69e0c4c490000") + bs, _ = hex.DecodeString("1f8b080000096e8800ffd43c5973dbb6baeff915a89a965422534eef9dce1d2b4ea775d25e9f6c9e38c98beb074a8424c414a880a06d8dabff7e3e2c240102a4683b6953cf9c531bcbb7e3db0066fce8539e12cad194655739660788b3028fd02ca39cd002977fafd32217ff537fa347e307e3478b349bc6297a7880e6719ac3a2982e8a3466fa6fb1e84150c02f396764c683c98307973143f986cef892d0053a2c7744ab2c29521c06d55c304267e7c389dc50b0741a03984314309c4b38d5ba4810cab234c52c0c4ecbd123ce5280302fe06f9251143ecc67d91a287cb8e47c3d44370f10fc08d86b862f9fc75c00df9f54a30bccdfbe8421c16b3d2a90c78c2bca258b40899854d00529144b84392cb8d94e1a9373b270c7579be3e782b3c01aa559820590b3730f90632a18b5c8d3f398b18c79f6e518d31762cec50492c7a94b16c3ebace4428e8fc7e814d44817399ae279c6309a66599aa334cb2e608473cc6c8c9ccbc5408b1cbf21c9010a5e919c630a80403bc0e10c6c456a0d9db08c67b32c456a05fa354940dc39ce6121dfacc10e038eaf39fca5b5a00c713b3280bf8eaf4f314d5e4ed7b901fe6dc11799d0d93ba1e6576445380a5f92dfc6f9b0864d8bd514b34ee8ef005e4c8f29307a19a7a706063583ca2914de1ab2b61c3f703d7977f827318be180a4eff0e7021699c20191211010eca4899451bda43f7c0072b484938c1b9217c07f2729466a5629a0bfe80d0c7f484ff32ba52f683c4d716220515308e63238edb8862bccb393ec57d9ac05a69cb90bc85331fe9bf2a4a6858b61548f9be0acfd1f4ee8894b8e1a4162d2bff9dcf6438b829cfa8f9f3e55a6f83e1cdfe7c87db01915d07e2df81243f098c5d2f57eb058de09f024cef3ab8c25dd408d551af0ba1ee9a4f6fd2bf370c100fafff7ef4f4e11783404887ae9ba1477155e205c9c16b319c6094ec232ba881f3247e177329898a386aa08253c1c4eeca930f89e620edc5c489f1d0c85938ed330589204078dd56ea8123f5b8b843a70dd928c7a633711bec86893b27545f67b0cbec195573bb10cf382d13626fd626e93e5cd349e5d242c5b83aa011f581628fb026fa659cc129dc06c5b44ddc560153ee7c0c7522cae7210933699874400302c939bc72818e71b38882ba03117d694e761bd398979dc64ce36bb894fb10a229021f61b240f55aa10faa9d3c04b0599fbeadfdf4e3f41548a40667968260dc308ced28b78b6348093c4518c57003219f94580390c6000d3190c7c78777c94add61905072020f5158f210609f78c24e78e249a5c99bffb6934b2bcde9a12d923cdae00bbc83521bbbb0a87236b054f6032146bf6aa9c7488c6e8c9fefebebd9224dafd943f460a0bfbbd66402844e63ac56dcc6605afa6ad79e11441e28850d42660e9e1c464b48cf3b75714f2b835667c2315e5592f7ecada62e2cc6e9d11ce362d500452a1d28ab9d7315f46abf83adc1fa1ff438f9426e48a63fadb86e3fc7dc621acef79d2756795103d4f862e8516e64a70eda821b9ea83db5ad68a7c8b20fecd9628c46da27584b2df9705cf42571b96393d3e6ca07321d806666e50438d73e8334ebba86abab15d87551544bdcf69b38cf2a3db4e1e344b25c8f078915bee5e0c37639bc82db2b9ed95c43af04b8750991534c1734221f9f3873e4858e8059c711a98d1c7c2e08206dd40d100a7f83b81a215f229cfd66bc0ec852ccb76ae7c4c203cb3078d9caf7797cbe57fcff6cf239e7d0004ec085403c1e6b19a00bd4c73cec227c389cd87de7e8806a23c84646080fefa0bd5a3c7498a074d5ed43498d90085839a4ad12c38c16c06f1235e60ad98c768f0c370e0e5568bc460a819dcd7d9510a09e7d7d737a1f3ec2b283b11f518fb62bab6141610504d2b667d0e83b62cae06932bbdb7425a33b28ad9c60bc9233e5b85b6497c753d42146f63d8235dd55793f140c2dfef07d5d2e07ac6812b5801d1c86b26e258291463df0283864953ac32dacdd30cf24740e3778aa281e5718a62f8686e65f6825ae1e4619d2738ea0dd11bf8eff1f373dbe6c4c2a66ccaf148e48c29966885104114bea0595aca31454222412322212c1a99eddbb47342a138284db410807e18362176389ce039c935ef9523f688f5782658fa370835bbb8ad3c75dd741ba1ad2095ccdba5e53aea6f565cae6fec2533d711ee9499e9175d918946d0df2c314d98149aee4375f9f55f3af46d88fc1f63a1e1043a63543b2b1ff14e3508626c4aa5a119770b2d64ea159798e500db47dfd7144f4a20f07e6cc7bd43cf106b1381d8918ecdaee060250a26799562deac00849463b3fd21e82dd1d25a9448419d58a5866051c38d524c177c09e9167ad2c271951c7430aaa1419adc6a0c6fe295cb6f9735c0fa168acc251d44d92a2e3375a8707ff6877cbe24f91b1fa54d9d50a5bac6b6b021619d73a9c5bdb2ac4158a8f2486eb293fba68876c9a78f707a4b0627841b1df9969660187c5fde9af56d53caa6f4d09be0c69778374aefad62a3bd692d8adeaee5f98fd49d8176d5968b70579d7216e5eb94f0301809cee2b571f0ae8d83771d71886560089eca7e9de54e1f0e508138fe73faf64d94cbbb4932df841625c311ba59e238015773806e82a38c023d7cef3dd85600228dd74096ba53187fca331a6c2dc43e8d0c44ff7bd0d60990adeb56795b6d72bb61dfd26dbf6d9bba45561ab2d9b6df75a3dc602d5f163c1147eb2ebcf9892a417abb33be46beb1e16ed2d9de9d7f7184bd21a7e1824bb0056360687ac7c3085f83dd25e1cd7654ba5597148102c4f7e21a0e8d578cd6b2539cce55cbb811fdade0eff25b131655177e7040d161e5efabe1e85346289c59e4b11bb1f8055092b1e8219c91132689b69cb8505929b63b39345b03c40df94df74992cfc66d59beccae023fac38d901cca7c51b535e407eb2a1f10ae8dff6d0a4dfa7daaaf4aff9fb059d60485df14e613b189d9b417937e017c9eeab3d9f1cfaa571de905e9e8fefea03621e0535dbc353c8954d3a8cdea5bc3211498eba36513762fe2eb39c93cdf006ccc6440f2eefc169935b57f23bd2837f223a378313243b9df65a669de0de4628c9281e2132b92da3bd6c5e63413ed93bab2aa7ddccb5c102d27886c3311a03c101a0ab46f6ca119d2a39302b2fe573e9774cc52a244276ae68d43da1bc4442043db5cfa8aa9060e2f1635f9fc45c7b46cecd2ad6164ac7f56eb9b9e678e22cd594db21b5fc99321c5ff4e89fa8db4e80d472a415c7eb225f866e986ff169519e311e96ad8b98e1bbba9e6fe124667c895949675ba26836205afd786f372ef21cd771b7d4a69d1e824072d674e577b06ada501118e6c4317bdab35d6332df6ea01e8ee334edd643e9130d8baab5d7742cdaa89d92dd586614c6b93f8516b7aaa222ec691aea16d6b58db6b21d47efc90aa367cea3d79dd6314b71cc5e9497be9d29a1f992d6a2f3cca65af785f6d0937349d6ce8029f78d25252d49eb9c11a821d28dab5638d426a9750e727be3955d3f337ef94d38971503fc7f15986695a31e35db56626e38ec68a6009c967a7a9df53198729dfd0ec9d7161101fc1dccdeae8cd33bea322e8714595cd2485580c3dc76d575069032a1731e4839199d67af85f4ac7420e7eedbbf3b9796426e3d6b0d41d117a835a01073d4b1530f1ef1dfb904fbfb5916c96a27cfea538375f62592558b662759d5589c6455ec725655e9c7d9b93b67dba611cf5cf3acbc936aad97559203c5972cfad1d1f2201c2a5cfe186a30a182d98d3a40079a90e66bcf667c153faa34f6d3ebaa48d571e562250bfd575b8a27249f0b6b2452d46d8eed9bcaf99450ba8f712f73fc32fd02ad234b071e6f6a5d5add5309ff963a593cf33674d4e345b2be1fbced033abdcdfb826e07c65bbe81aedef378df391b2bf47756f2a9aa5b32ec204a6be99644559f7cb512d5765db2e3424597f3567fb6b51c7560bda93374df7a5f3ddadabed2c7e375bc0edbcf502b00f9d6a3f9bebc972ac6e201d7ddf4511d50690ae6d8ce3ac1fc4c029285f283acd06668241f6defc33ac94e459a21d2301ea16949a571d919cb6b47f578afbeee443ffe88f482a97741f30e588379aa970f7dd5d2de13a774d3db9ee96d65fe6c4055115bc2757a320db86a5f0d596d7d566d05e918b29161b1453671f49c3088b319db48d4d55ffdb1d7009e99005c1a8421a7e6f7a12be3d3c8d493b85b85d7ea8c95615e93a18da5bce67711cac0b23231a6468ad5e826af4c64a9ca6256a2513c34984ed50136246a1391da442478465610aa004d0a454c61d1929005e1b9f85e6a56e6374223e22344e761a206bf6f955d0a40f9425ebd1994bfa6d942fd124f25eaa178f45ecd3cd92feb4581b9f1c01e6884ba5a41b65953646ecdcf8375f720a031ac88ed8f824beaf56ee37315ba2ef808c997b41e16e574c4b3dfc9354ec24a80d6aeda814827e0123425543cdaea4f4ff390cb41a986ca11b43d95dd47adef6d159467e0b27efa5ff4c8fc8f937fcb95e343cfd2890f6bb7947e1a8a27d801fa83dc86b43e34dd8398d7fd88e9a4e21ee85ffad19baf6e5906ca2eed41ec0976d9d90a8b8fefbf213b934f928dff7448b3b9f43e76761bcafa90741f33eb454b2711f7c07ef1158c2c5f42c8b9a38d59c47b9f52b5208dd3ab7893bf293f1ffffaf6ed7dc36f92bf8be2d9b2a017c7cfbf30b13e5207c6e337f93940cc6418565296cf0dc37174f364f4f376bc6896e072f14eb07a482ed695c85ed5b9ee34941330b27f5c06ea16747cf6e7f8cf3fcfc75e11882f8e147bba9bfff410fd4f8b6d18faf7dac7208aa2b1f8264901cca160c7a1097cefa7a192e2603c6893622213577289c3209305a1ea5c76c9d23c67ea9f613940c15130328665ff5eff3b2fd5306731cd6769913833b2d639687e79a59ec901ec43fdab81626b80c52b40c7c507f64f951e4561733810ff2ec000d1c59efc06e5706097bd671a6604a9eee0d9d3b1dcf94c63708f9b21a68292cf85ec4d1942ea92d1e702368b7f9562f15a50627091127a7150c3906218219cae4628e69c41963ce32c6d9a87188b1e82a273ccf2a8a0f992cc79553f8497045f7d8c537fcf527e0fd5a71b56fe8cc7e80a233863459ad080ab5414a4ed5dac2803c97e14ab08df34a4253ba91367abfe1ca1a64d75d82a46ce3b88e34bd53b45583092a33865384e3677224fb6d7dae9db4d03c99102f845a5e38c681babe4d368323bd762da9cff0b0000ffff010000ffffc0516aa298490000") gr, _ = gzip.NewReader(bytes.NewBuffer(bs)) bs, _ = ioutil.ReadAll(gr) Assets["app.js"] = bs @@ -63,7 +63,7 @@ func init() { bs, _ = ioutil.ReadAll(gr) Assets["favicon.png"] = bs - bs, _ = hex.DecodeString("1f8b080000096e8800ffec5ceb73dc3692ffeebf02e6eef95127cec88fec6dc9a3a9726467e3daf851567cb9ad543e6048cc101109300028794ed6feedd70df0fd98a1c623d9b9e483ad211e0d74a3bbf16bb0c1d9dd176f4f7efcd7bb972432493cbf33c33f24a66275ec31e111b1f2699a1e7b7a2d021371b1b245811446c93866ead83b2d6a4e8c8a3d12c454eb630f1bc5929e794892d1707e879059c20c2541449566e6d8cbccd2ffbb575544c6a43efb2de3e7c7defff81f9efb273249a9e18b980159189109e8f5eae5310b57acd64fd0841d7be79c5da452995ad30b1e9ae83864e73c60be7d38205c70c369eceb80c6ecf8d1e4b04328643a503c355c8a1aad4e339a9948aa4e8b988b33a2580c12836a136486f00029458a2d8fbd253dc7c7490a829cdfc10e869b98cd4b21924fe4f2127ee93732646f60a4070fafae6653d7aa1cc0115b4869b451349d065a4fcba749c2c5044abc7c1e661d331d3166dc0ced3331eb149830eca3c1ceb68690850cd7e4d2fe2424a5610813f217d218991c91ff3a4c3f3ecbeb96c0b4bfa4098fd747c4fb9ec5e7ccf08092372c63de01290b0ec87305d23e209a0aed6ba6f8d291b8ba63ff64f17f46df94232654adb8f08d4c8fc8a3c9372c69b49de064fd440aa9531ab0b257632eaf9988e501792d050de0ef89145a82421e10ef44668a330553bc801996645a43505036d4ee9009cdc2b9b1aa6bd41c55a75b8bf21aae5dc2820cd79694c38d947b6b4bca612985855421534e76428a165fb15cc996988fc8e1b3e64ad74a2c19ff9b6ac153a9395ac411ea14d8e4797b00ae8d2fa4bfc8c0299872285b6c15ce478573536b7504c6e22c11659f90eb34a6b0965c80ae337f11cbe0ac9807a8b6b364d0c7423f4ac5517c1519509daa624183b3959299087114a960faabc583c74ffe76401e3f3dc4ff1e3d7cd694a0a221cff411795263be90cfa3f423795a9517827c0cc58f8be2ab365fa066621252701c2d3d8fd912667b58297a83bd478755b1d57c1af315ca1f997cb65d5685800b47dc952fae05b9cb13f49a54981e4b20266a9ad90573227e7a78d8db3cec780fc7e463f01d3d4381379a5aed804d62ea76893b33abfad635def57df2a34c611515f17d5726e879b9c5d073ac717f50f38b9f215bd22c869d00b62866dbf115b5fedcb9b959c84b22e8be29884ee575e82061c19a63f80b4545e8cd673c59153568521ed12a400febe393ffe8f1dfad63276ee3f19e3c06b76f25e67e4fe7a4f4f3339dd0382e88453c04dbf63f82236e8eef7c5e66188cfe09840555f3de0dc2929be72d0a4eb2b8c647219dda4fab4b15e3312f9a874aa6a1bc10659dada7f9bef317afdd0ea4bf5ae1368d7a9e3fd4a9bc0cb9b927163a7d365b9492a70ab7a4d97401ff6863a06adee500091359633676be73989355701e9cc1961c86ef592a1f3c04aa7521aee2751ae1b64bca5f7e14e27a3a71dd6309ccec791812ec0e8e4eaa354e69368511b60e89eb306a4860f78239965bc32289c1014b51f0731ea29e8e981603799f326340d1f4a8b90572d59a57d17daff3d25166703947cd492e97ed39e5dd472e8e62da5065462ece129a47ad01df3b0a7de3cda6595c3d376babbad914c46361d7146cceb9b001efe3daa3cffb8e2b6dc0795d1c1029e23501247921085f12c102a63555eb67249f19b9a04a206cccfd634e1e64c0c150ef02fd255fbd12e8764aa305c295cd372613fb49085eac6667f57a100b8b89fddfcf876dfa87765b1f5dba6d358b9e346b2c9ef5e605176f180b59087bc0937929b161b2b843b47d413aff3162c4f19b29ebed4944355930065b303d672159001617d2101a007ea1e05227953b26498622cfa76364d908b6400672bf68929ecca6695319b64e1a315b6d97c99bc19c0cccd4a171f750aed3c20802ff7c9dd83ff9ae46528058b9e7be195d77d3684db4ae2731a36ac93f7a3d6bd52c683cd61ef29f1d8d67ab2c861d1d14b4a5cf0dcd2de861c7ca615bac491e200c40ccf1b0a2d0abe57feb3501b7548818538ff010c59ad3e74c8f308bcb4bec7082150ff0d7e4d50bd89ded3ac123a3c65104b846f0ef0f30655cb17146546f35604fcdddbab11903df314d75b147a7b0fb62d4fa970687c5068f853933c00270d0a07c9dbd95e4545e70c502bb4e9f888d8cdf51135d5d0d91ad29f9dc513835d46475a936b18e53ab268c4077b2d5488b556eb0db146e213a52caf09afea9dec40573607f29c4a61047b53d8243d2f5c6a415ffe5cf10ebf3147161abbfc3cf84344b55bb080ba3edbec2d03630a899dcab17b32910e9a11c36006c6321ad84a15fd899e4b43bcbdd27be943186733265a2c5c077b6669799974a7c1b0c043201d0db868bb863454a0afebf7623da818b1e43ba695656b15cb01623ff80321a3710f7b558490033c73fe75cfc32595972dff19869f03034bea06bfd264b164c5d5d9125161f90814edfae8dedb4e082e2d27e7bf3028964d296c70f32d8a33862a4764d69d83e5f4018412cb3d0475c1f4bda8ecade0268934b0bd43e4f220220e63505825dbe803cf00ca72585d7540378dc553bbaedec14f220c1b2fb1e40c65b8835bcf9bf98eeeeac3dddeeb6fabd9183dd6e5c623a0234e3d3b8e32cb13c243f7113ed4960058a1305849b60ecae7bba5c5e8ae28066c9853b2510b635385df4ba0d61fe152601031f9061217e8e70a1a40b0ba0108144bbb00d9286601900ccfe4025553c015b29c0a4d73a97b0473428ba31a10aecde018f5bcb8aa7492e24ef22c0dd0292664892c716b856b5a8c2b2bda7b042b49466289e284f32ebaa077d4f962b54c09f8b73c0070f7fd9218a1815478c8f241c534508814fbe3302986daef818495c6f73acdb514ecb4216da56dbb1587f685e5b203f88fb2b46fd43b87fc0a98ec5fe9d2382e7afc907c3e38de073c396acd7b08d2513f8b36d47eddf213e931daacf748ba393771ff6c1519066ef980ac00e5ae8021e05c06d45e3a3475757ff715bac6e84542ff262f29e1a766d8eb958a4b87c09034d0caeaee0e976b9cad21e9e3ea4bb732433d361a9775fddc053b191e7fa00f49f0b213311b0b7ff24778f4906560c30a0c77047738faf5623a9dad16c310e3965ea7c28ac1d8f745a1bbe7d6f693be82cc0c3676f13a7defcad7d0f39045606898754ac982a69df1d20be5c6ea17e6b9ea47d28f2df4ce95dfcc7b9ebd71f840fc0e4ddb1dc0da039876bf38df98b01ba1107b3768ef660b6821143b84a9a88290bec773a9dfd32b86a789dc7bc8c6cf4eec55ce3f4c875cdcf98ea80ad3bcaed83b83f1c82c364b1eefb6660425fdb53a1acb16f63516f69dfdffd24740b433d7afabb0768405eb00045a27f6e1ac82f93ff7fe06d13b7fb00765f014640b6a0ef4e5afa2758d872fa73a7faed4e7da6cdb7cf58f45229a9747f8a05c33af71e771233b132119993c3af31d7e28d343c609f956351874bc03842a51affb8a0360bedf2124a273ff28481ed01966147def747497204d1c3d5d55191ab062863a9381361bc76500b3b59515b0dbff9fc8a4aa3497faa455d7d6dd283538471c94bed7df7ed3f6f37b3a2a1d3659e51994789c5dfdaccee71e99545f649fe089364619e1a4e6ad98b77ba8adec9af1cca4aac9b824be56a26615a30937b16bc2b70340516752033a5d9a4bc9f3011cc4cbdf9699662b2299992efa4ca926e06d7a821348cb1e226ca16130021d380c649342d879a2a060ba6f124f707d0726dc87b57b0e3681b180a80fe4aaaf5349441866028cf677d517fbc1926b9d619b2f86dd6970cb8871160b180e58091939e34c8cda974f6981e2219a9ce9c2722890c695caa73192fb836d67e4bfb774d9734643d7a6b2bfd90d358ae7a3db86b90dffe18f0e2ae0d7a66b03c1a33d047fb7f71ded130f0e869b35b6fc8b8cdedb08f5099586df0355f89fef8eea4044d6e6f6bf899e8e9809be9e3ad1b32a5cda1aa0c3bcdc00f626add821104be0704960b036d46b82694a44a022849c805280a5983469057205b054b472a903769107f0fb86e0db4ef452c8e79faacc1473ada5b36dca6cb2ab3a970362fb0579f54d9e06bd0262e96f20674a99d26d8127d21815b511ed0904ae613f2ceba5912c938dcf7da1739c6032b5f64307f0deb5e1cc9ee7fe9eba9d7cd1529a48377f36266d8adac7e95c80bc35be731d9d772db97bc189b0cee1d45e072fd157704fd783f4bdf94d526600ba176798a088022e125298f50c5a9ef001bc4992a031eee1940eafa591f3c456d020c8c09e8c7de5d94042cc7cb8f00f73baeaf50b4ea3e435d1b5aa44651c240af87d40e8ab5942ac92f03e14f2fbfc28991e84bbbf46dfba8d1c40ec5ab7b1b0ed8d2cbfba093be851cf7f12655416822ecd9c3e4af5c9cd39887e4debdbeda902bb36e9fd92298a20b08fb604837b9572f306c0305c5444b5bd7e9c1458a69edf97b938e5c2b46815679748abfebece5d77949f386a557bb246a79b7e951a05e99c293699c587ed482374d7fcbb86221380d3ba3ce3ceb21f3c0e25f802327f81f8661ada9e0494f6758882e832813672ea1b4737c6123d69c366c12a9bb1db72d1daa67b5dc4a7efad4b792a9b25cc00cf1fe81708b85288721782116de04401d40cf12ef2162cc8c570abcc24ec04e9db390aec2be7bb08426e41479076c04dd42aa23fc09e4a4bd9b0c2ee801003c09427f381995b1d5337babc09362f136296b933fe0086f5200538b988ab381f15b417cff1953bfa96db40c8af921f87662b351a431482fb2c9bec7def732295e91e6768054faac6094da5397a2d2afebbd5a77ea2ef1086dc09b634a636ed8854204716673fcb43d926e5f30d997eca83bfdc778ee79f1f31a520cd720361e58b1c0ce82a788619f4320c7c72459172ea71a74577997733d35ea9a727f69ed105f2150084152aaf0e64fdf25cbcad7f0f408cf0e8a0ca472fa18b06cec988ba7e8085e2065ca6e3e343312c3b280d8081fb4708d3a800b9f931fb1e2b329d2ea1c830d6fd04d7a3f4580a0dcb55c08b7f04e139af30139632c452d4c38f819135143662c99636a996d309bc293bb1fb5b0b305f1357c95e6a0c846ca09411751a8b5e52e2701f0fd685073b7b87d54a38d8e7ed3c9581f3ce839281c750dab3cdbae5d67a4e72cbffed9b4889a172d90c02e6786a740bf17968d996f7988d98703c75c124be4793b49fd0491e5a619f56ff1b8b114ca79cae2a5b765e6f674c45d74c3bb5c7591836b80a863f49d5bd0e9ac9df3f5c292e87271fd5382546e0f1db0d59fa1c3e8d0a19ee2be97006288e0fec208cc65de4b1851119ae0cf5618d1533b228c704df1a66de3d6d6c660a2e40a37f0e6fe9f1f37eb96bfeb2c4771ab6e20d2d8b2e7bf773730aab0826482ff9661fa452aafb7fdb71bb56f3eb4245a42fd9eba0aea9fe20546e011a6ca97f85199a53bca24e565caf584bcce774c2cd72052dc34dd271f1cd0b3693d43c879cb341d6e7742b10ec6a273555fe372c776ad3e6ba07a8030a4867d53b8f13061ac29e165d34dc6e4ea479a13366e1814168c3529dbb96954ff9ebea8ccaa301cd76e47d329ef298e08cc3fd76a9ce886ecc6d5569683cf888b9bc652e0497b030db13a4c94a909f98983bd80f6048a59d4ce9784e3f91fd3f6223f4377e350a7e1204bfb69311aa071cd0290cafcdfb3a9fd5b44e0196661517cd9507e9c0b2dd7352e93b92d2d049ab67807c3714c6f359d9ad2758c274539dd7e945d7f711bb1e06c213ff69d97f4aa7a4ddd9d86961406b4b4ba3b56bfc6efaeba75c71cb0aff1c73eeef21f1ea0a40a228000156aa964825a0378539304a0126a627502a30fec4723da0d8af0c8454636c0b9c83555e3158052bfb52982bcdce9df54589fdf27b279144392ea5bdb76feebc6e4d7bd2ebe06108e4b6007b2995b45da96d7b9f3d34e3edd8f3e9cda19b8ef7be02470d9ec454217b7d68c11df487e89f0bc9a425f908e136fa03a7c49663d1cea1fafb9136e995b31abd70b9bf82b72f881ce736d8d22c0176aa0bef89db77366cf1c2d8248a8092270b5e057e375d91b818ca3f0f544e2f9b79f9ac8b4e673ff8cc411997f66e43dfa035b37197917dfa61a7a479b57ff51c3eebe589854dff3da73143cbc79d5931ffbea3b7766fb9bb5f07eb15fe5ab8c4eb15cf0de6da9f6ea296f38c145c033eafb88a2ef236eedd4087b7fef7e2fc9e6e60bc031efcc43bc88513ddb4fc922921cdc33cb5d13f5b6456813faaf9ae2737e93a5d866ed97aa266f5397155dd1fc65f0e8bc47150696acbeadf74a742165bc596afd7578eba425b74db2d9803276637f10526c904d3fdae82dbc09b55f65fcf4e634bfb72e85a95d4815fe8eede21f1f5efd516de23aacdfa43d6c85ceb70d1daff5adccdf2d381c0db3f0a7fb08bafbc02da042fc309ffdb0f8af0ed4d9da79bbe1afbf654cadfdc793c3c993edadabef97ffdafe7cf9c67e344d5b0d80577bcb05c08dfd8afdff010000ffff010000ffff55879ee8d65e0000") + bs, _ = hex.DecodeString("1f8b080000096e8800ffec5de973dc36b2ffeebf02e6eef3514fe4c847f66dc9a3a9722467e3daf8282b7e795ba97cc09098212292a00150f23c59fbb76f37c0fb98e18c478ab29b0fb186381ae84677e3d7608399de3f7d77f2e33fdebf22a18ea3d9bd29fe21114d96c70e4b1c922c5d9aa6c78e5a25be0e79b23445be48b41451c4e4b17356d49c681939c48fa852c70e368a043d779024a3c1ec1e21d398694afc904ac5f4b193e985fb57a7aa08b54e5df629e317c7ceffb91f5fba27224ea9e6f3880159189125d0ebf5ab63162c59ad5f426376ec5c7076990aa96b4d2f79a0c3e3805d709fb9e6e180f0846b4e2357f93462c74fbcc30ea180295ff2547391d468759ad14c8742765a443c392792452031a8d67ea609f7915228d9e2d859d00b7cf45210e4ec1e76d05c476c560a917c215757f04bbd15017b0b233d7a7c7d3d9dd856e50096d85c08adb4a4e9c4576a523e79314f3c2871f279e855c454c898b63334cf44af526042b3cf1a3b9b1a42e62258912bf393909406014cc89d0bad457c44fee730fdfc22af5b00d3ee82c63c5a1d11e77b165d30cd7d4adeb28c3907a42c38202f2548fb80289a285731c91796c4f53df3278bfe3bfca61c31a672c913578bf4883cf1be6171a3ad87937563910895529f95bd1a7379c392481c903722a13efc3d118912a09007c4391199e44cc2142f61862599d61014940db53b608962c14c1bd5d57286aad3ad45790dd72e6041866b4bcac15acabdb525e5a094c25cc880492bbb44242dbe22b1142d311f91c317cd95ae951832ee37d582a74271b48823d429b0c98bf6005c693711ee3c03a7a0cba14cb151381715ce4eadd511188bb23829fb045ca51185b5e409e83a73e791f0cf8b79806a5b4b067d2cf4a3541cc997a106d5a92ae6d43f5f4a9125018e22244c7f397ff4f4d95f0ec8d3e787f8cf93c72f9a129434e0993a22cf6acc17f279927e26cfabf242904fa1f869517cdde60bd42cf1020a8ea3a5e7115bc06c0f2b456fb0f7e4b02a369a4f23be44f923932f36cbaa1070e188bbf2c5b520f7798c5e9326bac712880e9b6676c9ac889f1f1ef6360f3adec332f9147c47cf50e08d26463b609398d85de2ded4a8be718df75d97fc28525845495cd79625f4a2dc62e805d6d83fa8f9c5cf802d6816c14e005b1433edf8921a7f6edddc34e0251174df144427f33a7490b060cd31dcb9a449e0cca63c5e163568520e51d2470febe293fbe4e95f8d632776e3719e3d05b76f24667f4f66a4f4f35315d3282a88853c00db763f83236e8e6f7d5ea6198cfe05840555b3de0dc2909be52d0a4eb2a8c647219dda4fa34b15e3112f9a0752a481b84cca3a534ff37de74f4ebb1d487fb9c46d1af53c7fa853791570fd2099abf4c5745e4a9e4adc92a69339fc471b0355f32e07885992356663e63b2be7646149c4fd73d89a83e0034bc5a3c740bd2ecc65b44a43dc7e49f9cb0d035c572bb6072c8619be0c0282ddc1e109b9c2a94d2730d2e8a1715d460d0dec5f322b82d6f0486270e05234fc8207a8b75b4c8fc13a9c31ad4101d5a839fa62d99a5fd17d4bc1f0f172f9247d68da1e361497e4f5a91db5aee47b918b0a338d6a366a7e62b1e84cce76df522692294da51ea92c0b681eb606fe6029f48d3b9d6451f5dcacadeaa6131097818513f009d6c50e7847db1e7df2775c2a0dcef5f28088245a1140ba9709e10b92309f2945e5ea05c967462ea94c10d6e6fe3b270f32e0208cfb407fc197af13748ba55301c2954f6a4c2672e300bc6ccd0fd4eb412c2c22e65f371fb6e9bfda6d5ddc724cab69f8ac5963f0b6332bb878cb58c002d8a39ecd4a890d93c51dacedabd2d98f212396df4c9add8884549139630011e8050bc81c62854468427dc057145cbe576d1724ce50e4f974b4281bc116cd40ee974dd2de7492369561e3a41153d676c1bc19cc49c34c6db4601fca759aeb84c07fae8acd9f7cd7252940c07c67b9195db7d3684db4ae2711a372c13f3b3d6bd52c683cd61ef29f1d8d67cb2c02c4010adad2e786e616f4b063b591182c4c1e214c414cf4b8a2d0abe57fe93501bb5488685387f000c59ad3e74c8d308bab2bec7082158ff097f7fa14d08359277864545b8a002709fefd01a68c2b36ce88eaad06eca989261a6001f88e68aa0a0c91023ac0a8fa4f0d0e0b0082853933c00270d0a0bccd9e4f722aa75c32dfacd3176222f7f75487d7d743646b4a3eb314ce34d5595daadd6daa0d73d09d6c34d262951bec36855b888e9432dcd23fd59bd86013ec2f85d819e2bcb647b048bfde98b4e2d3fc5969c953c4adadfe16df13d22c95ed222c0c37fb0a4ddb00a56672881780480fe5a001b01b0b69240cfd82ce2427dd59ee3ef1858830dc14294b5a0c7c676a769979a9c4b7c1802f6200e56df88a3b562845c2ffdf6c443b70d1634837cdca3212f336e0fc1b94d1a811096cc54a0c1836fa39e7e2176f69c87dc723a6c0c3d0e892aed4db2c9e33797d4d16587c40063a7dbbd2a6d39c271497f6db9b174828e2b63c7e10fe1ec51121b52da561fafc06c2f02391052ee2fb48d076b4f80e409b5818a0f675124900626e2910ecf21bc803cf985a5278431580c75db5a3dbce4c210f120cbb1f0064bc8358c399fd83a9be00b0d3ed7eabdf5b31d8edc625a64240332e8d3ace12cb03f213d7e19e0456a0b8a480701e86ddaaa7cbd555521c202d7862a3f3c4b406a78b5eb721cc3fc32460e003322cc4af112e947461011422906817b641d2102c0380d91fa8a492c7602bcec0f988393a42d18d095560f7f679d45a563cedea3fa8d83520698624796c816b558b2a0cdb7b0a2b9296d20cc513e5496b5df5a0efc962890af873714ef9e8f12f3b4411a3e288f1918465aa0821f0c9b54600b3cd151f2389ed36c7ba1de5b40c64a16db51d8bf587e6b501f283b8ef30ea1fc2fd034e752cf6ef1c11bc7c433e6a1ead059f6bb664b5826d2cf6e0cfa61db57f87f84a76a83a572d8e4ede7fdc07477e9abd67d2073b68a10b784c006e4b1a1d3db9befeafdb62752da43acd8bc907aad9d61cf3649ee2f2c50c34d1bfbe86a7dbe52a4b7b78fa98eece91c87487a5de7d750d4fc5469eeb03d07f9924224b7cf6eeefe4fe31c9c08a0106f418ee68eef1d56f28643b9a2dc621674c5e0c85b5e3914e6bc337ef554d0795f978f8ecace3d499bd33ef4987c0ca20f180264b264bdaf707882f161ba8df9a27691f8afc2f936a17ff7161fbf507e1033079772c770368cee2da7c63fecd00dd888359334773305bc188215c2574c8a401f63b9dcefe36b86a789dc7bc1c6df4eec55ce3f4c876cdcf98ea80ade79de2ad83b8ff380487c96cddf7dfc084dada53a1acb16f63516f69dfdffd247403433d7afabb0768403e613e8a44fddc34905fbc7f3ff0b68edb7d00bb3b8011902de8bb9396fe0116369cfedcab7edb539f49f3ed3316bd925248d59f62c1b0cebec7f522962c754866e4f02ee65abc159afbecab722cea70091847a854e31f17d464c95d5d41a9f7238f19d81e601976e47c7f14c747103d5c5f1f15b97480321692b3248856166a6127236aa3e1379f5f516934e94fb5a8abaf497ab08a302e89a9bdefbefbfbed66563474bacc332af33cb1f85b93793e2efdb3c83ec91f61922cc853d7492dbbf25e57d13bf99f4359937553b0295dcd2451036672cf8277198e26c0a2f2452615f3cafb135ec2f4c4999d652926c39209f94ec82cee66708d1a42c1184baec36cee010899f8348ac34939d4443258308527b93f80962b4d3ed8821d475bc3900ff49742ae2681f033044379beed69fdf16698e44a65c8e2b7595f52e21e4680c502967d464e7ad232d7a7d299637a8864843cb79e88c422a051a9ce65bc60db18fb2deddf365dd080f5e8ada974034e23b1ecf5e0b6417e3b65c08bdb36e899c1f268c4401fcdbfc57947c3c0c3e7cd6ebd21e326b7c33e43656cb4c1557c99f4c777272568b27b5bc3cf84cf07dc4c1f6fdd90296d0e5565d829067e1053ebe68c20f03d20b05c186833c215a124950240494c2e4151c80a3482bc06d94a583a52813caf41fc03e0ba15d07e10b228e2e98b061fe9686fd9709b36abcca4c299bcc05e7d926583bba04d3c59881bd0a5769a604bf485046e457940432a997be4bd71b3241451b0efb52f728d0756bec864be0beb5e1cc9ee7fe9eb29d8cd1529a483770723a6d9adac7e95c80bc31be7e1ed6bb95f9f0e2c340f3eedb05558626eb49fd5be81956d25ff373a9b17deaf01ca69bee0becda17e100754852fea4782d54be7e6a9e0d7ad7fbdc1259834c17f1090b7ae259a477cd3d612cfd555bc82c5fc42fc304bcef13470702ab55b469690bdd745a0dc059012cf130a51a7bd7bf4494eba949dc9167cf60426a3d2becb20c49c0603308c7949745492772c2eda496627914074da8943b6b51aa32918d10f22ae22dcbf732604211ddea73876eee314c1bbbcfa0cd16b67272facabba2e5457ee16a95194f0dca287d40e7e7221649cdfbdc39f4e7e631a0df4955993b653a8d1c40e45268a896e4de9d54370b1ae41d00ff1e26241c84bcc519af7679e5cd08807e4c183beda804bbd6abf82c0d880ce19acb9907672af4ff114027dcc29407cacebf4e0498ab734f2d7801db9568c02adf24d00feaeb397df9e6f790ea77627dbf06eb2fdc00564125fb4e0c4f29343bcd8fd29e39205603d66469d79d64f8006167f8d133307979d61eb1ea6e734ce1cc0e4b401f3a4d6696dcaeeeb592dbb925fbef4ad642a0d173043bc4e93d8c542d06efc243168dd07ea80e11778ed178f80f0868c63b47b46f26b64e0b4ac2d0b5b6d5ea819721e39430900e087ceb8b5e04f202acc0701c0433c82a84580e81f7bc38c0d18f0eca7103082bd180b0105dedac1210fc8396329ce34e630a80ea9265316cf701f330da61378b23780802fe8cf82e6c41507396821bcadb3f3362c85b138afd0b675d6d55c105802bcc904b39d4734391f9857eb10adff8cb7df37ac35658af9590805d65b711ac1428726d9fed8f95ec4458a426eb848a5cf6c47d929b52962fdc6d96b2667f6125da234ec0b98529c7ba24283fd283339b6cabc126a5ff0da97eca87dfb86e7292f8b9f5b48315881d8b86fc40288004ff1833e0f468e8f0982162bea6ad05de55dcef54ccb2de5feca380e7c85478962299578f3aeef1276e51c797a846777450660397d3c3058db31174fd111dc56caa4d92d69a6051e8bf8c49cb08116ae500770e173f223567c3a415ab700fdcad728b51bb4f482e5377c9b8b5f7318c52ebdcbf1f419d0ef3ba0be7350b59c51fff68b3eb4d818ce58b47036ccdc1cc4d93b95786db02e72b0020870475fab86cd256ba7179e1a125d2eb63f904ac566bc8dadfe1df076fd9ac35e50f710c1fd616fcc67df0bf6ae0879f8b385bd7b6a47606fdb146f5b376eeead45e02557b88934f7a0fc95836a39a2ce7214372b07e0f9867de783bd855361719225fc53862938a9d86e0b6a376adf7e6949b4c4c73d75153e3ec34bacc0a33d2d011fb2b0c7d9a4bc50bbf2c89b1c5362b9029122acb49f25b160c3a4760da1b70dd3b4d8d10ac558be4188b2bec625a6b5adbe6aa03a481d52c3be29dc38541d6b4a78e1789d31d9fa91e6848d1b068505634dca746e1ad53f27a79559158663dbed683ae55dd511d1ecd75a8d15dd90ddd8daca72f019b159d3588a88cbdc4244bc081365d2233f71b017d01e5f32831cf982703c0366ca7ccc81a1bbf108ea9de6204bf3f93beaa3714df1a473f6cfe9c4fc2dc2d60c33f128be702a3f2087966b1b9709fd861686e1a67807c3b14c6f349d9ad2758c274539dd7ea4577f791f32ff7c2e3ef71d32f4aa7a4dddad86961406b4b4ba3f58ff9483bdeed81365f7dbd7f8b3127b0114cf1b5209d0dc47855a4811a3d600105424060c839a581d58a803f3e1907683e200c19e1d98b8fc32d75485d7404afd56ba083472a77f53a1657ea7cce4d20c49aa6f6ddb39d06b13a0f7baf80ad0312e8119c864ef15a97b4ee7de573b01793ffa70666660bff18293c06533974989391caa1923be95de2144ac8e0a87ceac1af4ba0758d514fa8eb170e20d54872f4a8d8743fde33577c20d734b66f47a6e92bf931c7ea0f35c19a3f0f1a52aa82f7e8bf08299233a83202072f64370b5e057a355d91b818ca5706f5022b71d22e7df256b22d39acffd23444664fe9521f1e88fbfdd64485c7c276de83d7d5e7de7e2e1be2095541f7ddb73783abcabd43353fbea3b179afb9bb58078b191e4e2476f55ae44ef7e517b919237f4503bf100f321c2db8708283b3589b95cf9b09764735704449777e601de92a99ecd778811e20d6e66e576860ad522b40e96574df139bf6654ec7fe63362debbd4a6ac57347f193c57ed51858125abefb7bd129d0b11ad975a7f1dbe5e6fc96d9d6cd66cffbbb13fb8d7af914d3f0ce82dbc09b55f66fcece634bfb72e85a95d0a19fc8eede26f1f5fffa7dac436acdfa43ddcb9d71e5b7d50f5778bda46e31ffc69bfa06f339400aee15713cd57e97fb568cbd4ceda0d7ffd9431b9729f7a87deb3cdadab8fdfffdafef6fdda7e344d5b0d8057730509c08df95f20fc0b0000ffff010000fffffa536c1f13610000") gr, _ = gzip.NewReader(bytes.NewBuffer(bs)) bs, _ = ioutil.ReadAll(gr) Assets["index.html"] = bs diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 5bd57528f..49e5b8ca0 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -19,6 +19,7 @@ import ( "github.com/calmh/syncthing/logger" "github.com/calmh/syncthing/model" "github.com/codegangsta/martini" + "github.com/vitrun/qart/qr" ) type guiError struct { @@ -80,6 +81,7 @@ func startGUI(cfg config.GUIConfiguration, m *model.Model) error { router.Get("/rest/system", restGetSystem) router.Get("/rest/errors", restGetErrors) router.Get("/rest/discovery", restGetDiscovery) + router.Get("/qr/:text", getQR) router.Post("/rest/config", restPostConfig) router.Post("/rest/restart", restPostRestart) @@ -289,6 +291,17 @@ func restGetDiscovery(w http.ResponseWriter) { json.NewEncoder(w).Encode(discoverer.All()) } +func getQR(w http.ResponseWriter, params martini.Params) { + code, err := qr.Encode(params["text"], qr.M) + if err != nil { + http.Error(w, "Invalid", 500) + return + } + + w.Header().Set("Content-Type", "image/png") + w.Write(code.PNG()) +} + func basic(username string, passhash string) http.HandlerFunc { return func(res http.ResponseWriter, req *http.Request) { error := func() { diff --git a/gui/app.js b/gui/app.js index efad57307..e64d933ac 100644 --- a/gui/app.js +++ b/gui/app.js @@ -285,6 +285,10 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) { $('#editNode').modal({backdrop: 'static', keyboard: true}); }; + $scope.idNode = function () { + $('#idqr').modal('show'); + }; + $scope.addNode = function () { $scope.currentNode = {AddressesStr: 'dynamic'}; $scope.editingExisting = false; diff --git a/gui/index.html b/gui/index.html index 3c6a11e45..d3652438f 100644 --- a/gui/index.html +++ b/gui/index.html @@ -80,13 +80,14 @@ @@ -373,13 +374,36 @@ + + + +