Add Edit > Show ID with QR (fixes #243)

This commit is contained in:
Jakob Borg committed 2014-05-21 20:06:14 +02:00
1 parent 4235175966
commit 6fb05fc82a
10 files changed
+1782 -17

No files matched your search

+13 -2
View File
@@ -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"
}
]
}
+815
View File
@@ -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<<uint(7-x&7)) != 0
}
// A Mask describes a mask that is applied to the QR
// code to avoid QR artifacts being interpreted as
// alignment and timing patterns (such as the squares
// in the corners). Valid masks are integers from 0 to 7.
type Mask int
// http://www.swetake.com/qr/qr5_en.html
var mfunc = []func(int, int) bool{
func(i, j int) bool { return (i+j)%2 == 0 },
func(i, _ int) bool { return i%2 == 0 },
func(_, j int) bool { return j%3 == 0 },
func(i, j int) bool { return (i+j)%3 == 0 },
func(i, j int) bool { return (i/2+j/3)%2 == 0 },
func(i, j int) bool { return i*j%2+i*j%3 == 0 },
func(i, j int) bool { return (i*j%2+i*j%3)%2 == 0 },
func(i, j int) bool { return (i*j%3+(i+j)%2)%2 == 0 },
}
func (m Mask) Invert(y, x int) bool {
if m < 0 {
return false
}
return mfunc[m](y, x)
}
// A Plan describes how to construct a QR code
// with a specific version, level, and mask.
type Plan struct {
Version Version
Level Level
Mask Mask
DataBytes int // number of data bytes
CheckBytes int // number of error correcting (checksum) bytes
Blocks int // number of data blocks
Pixel [][]Pixel // pixel map
}
// NewPlan returns a Plan for a QR code with the given
// version, level, and mask.
func NewPlan(version Version, level Level, mask Mask) (*Plan, error) {
p, err := vplan(version)
if err != nil {
return nil, err
}
if err := fplan(level, mask, p); err != nil {
return nil, err
}
if err := lplan(version, level, p); err != nil {
return nil, err
}
if err := mplan(mask, p); err != nil {
return nil, err
}
return p, nil
}
func (b *Bits) Pad(n int) {
if n < 0 {
panic("qr: invalid pad size")
}
if n <= 4 {
b.Write((uint)(0), n)
} else {
b.Write((uint)(0), 4)
n -= 4
n -= -b.Bits() & 7
b.Write((uint)(0), -b.Bits()&7)
pad := n / 8
for i := 0; i < pad; i += 2 {
b.Write((uint)(0xec), 8)
if i+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<<uint(7-o&7)) != 0 {
pix ^= Black
}
}
if pix&Black != 0 {
crow[x/8] |= 1 << uint(7-x&7)
}
}
crow = crow[c.Stride:]
}
return c, nil
}
// A version describes metadata associated with a version.
type version struct {
apos int
astride int
bytes int
pattern int
level [4]level
}
type level struct {
nblock int
check int
}
var vtab = []version{
{},
{100, 100, 26, 0x0, [4]level{{1, 7}, {1, 10}, {1, 13}, {1, 17}}}, // 1
{16, 100, 44, 0x0, [4]level{{1, 10}, {1, 16}, {1, 22}, {1, 28}}}, // 2
{20, 100, 70, 0x0, [4]level{{1, 15}, {1, 26}, {2, 18}, {2, 22}}}, // 3
{24, 100, 100, 0x0, [4]level{{1, 20}, {2, 18}, {2, 26}, {4, 16}}}, // 4
{28, 100, 134, 0x0, [4]level{{1, 26}, {2, 24}, {4, 18}, {4, 22}}}, // 5
{32, 100, 172, 0x0, [4]level{{2, 18}, {4, 16}, {4, 24}, {4, 28}}}, // 6
{20, 16, 196, 0x7c94, [4]level{{2, 20}, {4, 18}, {6, 18}, {5, 26}}}, // 7
{22, 18, 242, 0x85bc, [4]level{{2, 24}, {4, 22}, {6, 22}, {6, 26}}}, // 8
{24, 20, 292, 0x9a99, [4]level{{2, 30}, {5, 22}, {8, 20}, {8, 24}}}, // 9
{26, 22, 346, 0xa4d3, [4]level{{4, 18}, {5, 26}, {8, 24}, {8, 28}}}, // 10
{28, 24, 404, 0xbbf6, [4]level{{4, 20}, {5, 30}, {8, 28}, {11, 24}}}, // 11
{30, 26, 466, 0xc762, [4]level{{4, 24}, {8, 22}, {10, 26}, {11, 28}}}, // 12
{32, 28, 532, 0xd847, [4]level{{4, 26}, {9, 22}, {12, 24}, {16, 22}}}, // 13
{24, 20, 581, 0xe60d, [4]level{{4, 30}, {9, 24}, {16, 20}, {16, 24}}}, // 14
{24, 22, 655, 0xf928, [4]level{{6, 22}, {10, 24}, {12, 30}, {18, 24}}}, // 15
{24, 24, 733, 0x10b78, [4]level{{6, 24}, {10, 28}, {17, 24}, {16, 30}}}, // 16
{28, 24, 815, 0x1145d, [4]level{{6, 28}, {11, 28}, {16, 28}, {19, 28}}}, // 17
{28, 26, 901, 0x12a17, [4]level{{6, 30}, {13, 26}, {18, 28}, {21, 28}}}, // 18
{28, 28, 991, 0x13532, [4]level{{7, 28}, {14, 26}, {21, 26}, {25, 26}}}, // 19
{32, 28, 1085, 0x149a6, [4]level{{8, 28}, {16, 26}, {20, 30}, {25, 28}}}, // 20
{26, 22, 1156, 0x15683, [4]level{{8, 28}, {17, 26}, {23, 28}, {25, 30}}}, // 21
{24, 24, 1258, 0x168c9, [4]level{{9, 28}, {17, 28}, {23, 30}, {34, 24}}}, // 22
{28, 24, 1364, 0x177ec, [4]level{{9, 30}, {18, 28}, {25, 30}, {30, 30}}}, // 23
{26, 26, 1474, 0x18ec4, [4]level{{10, 30}, {20, 28}, {27, 30}, {32, 30}}}, // 24
{30, 26, 1588, 0x191e1, [4]level{{12, 26}, {21, 28}, {29, 30}, {35, 30}}}, // 25
{28, 28, 1706, 0x1afab, [4]level{{12, 28}, {23, 28}, {34, 28}, {37, 30}}}, // 26
{32, 28, 1828, 0x1b08e, [4]level{{12, 30}, {25, 28}, {34, 30}, {40, 30}}}, // 27
{24, 24, 1921, 0x1cc1a, [4]level{{13, 30}, {26, 28}, {35, 30}, {42, 30}}}, // 28
{28, 24, 2051, 0x1d33f, [4]level{{14, 30}, {28, 28}, {38, 30}, {45, 30}}}, // 29
{24, 26, 2185, 0x1ed75, [4]level{{15, 30}, {29, 28}, {40, 30}, {48, 30}}}, // 30
{28, 26, 2323, 0x1f250, [4]level{{16, 30}, {31, 28}, {43, 30}, {51, 30}}}, // 31
{32, 26, 2465, 0x209d5, [4]level{{17, 30}, {33, 28}, {45, 30}, {54, 30}}}, // 32
{28, 28, 2611, 0x216f0, [4]level{{18, 30}, {35, 28}, {48, 30}, {57, 30}}}, // 33
{32, 28, 2761, 0x228ba, [4]level{{19, 30}, {37, 28}, {51, 30}, {60, 30}}}, // 34
{28, 24, 2876, 0x2379f, [4]level{{19, 30}, {38, 28}, {53, 30}, {63, 30}}}, // 35
{22, 26, 3034, 0x24b0b, [4]level{{20, 30}, {40, 28}, {56, 30}, {66, 30}}}, // 36
{26, 26, 3196, 0x2542e, [4]level{{21, 30}, {43, 28}, {59, 30}, {70, 30}}}, // 37
{30, 26, 3362, 0x26a64, [4]level{{22, 30}, {45, 28}, {62, 30}, {74, 30}}}, // 38
{24, 28, 3532, 0x27541, [4]level{{24, 30}, {47, 28}, {65, 30}, {77, 30}}}, // 39
{28, 28, 3706, 0x28c69, [4]level{{25, 30}, {49, 28}, {68, 30}, {81, 30}}}, // 40
}
func grid(siz int) [][]Pixel {
m := make([][]Pixel, siz)
pix := make([]Pixel, siz*siz)
for i := range m {
m[i], pix = pix[:siz], pix[siz:]
}
return m
}
// vplan creates a Plan for the given version.
func vplan(v Version) (*Plan, error) {
p := &Plan{Version: v}
if v < 1 || v > 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<<uint(i)) != 0 {
rem ^= formatPoly << uint(i-10)
}
}
fb |= rem
invert := uint32(0x5412)
siz := len(p.Pixel)
for i := uint(0); i < 15; i++ {
pix := Format.Pixel() + OffsetPixel(i)
if (fb>>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
}
}
}
+241
View File
@@ -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
}
+401
View File
@@ -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<<nx-1), nx, false)
}
func (b *bitWriter) repeat(n, d int) {
for ; n >= 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) }
+109
View File
@@ -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<<uint(7-x&7)) != 0
}
// Image returns an Image displaying the code.
func (c *Code) Image() image.Image {
return &codeImage{c}
}
// codeImage implements image.Image
type codeImage struct {
*Code
}
var (
whiteColor color.Color = color.Gray{0xFF}
blackColor color.Color = color.Gray{0x00}
)
func (c *codeImage) Bounds() image.Rectangle {
d := (c.Size + 8) * c.Scale
return image.Rect(0, 0, d, d)
}
func (c *codeImage) At(x, y int) color.Color {
if c.Black(x, y) {
return blackColor
}
return whiteColor
}
func (c *codeImage) ColorModel() color.Model {
return color.GrayModel
}
+151
View File
@@ -0,0 +1,151 @@
package qr
import (
"image"
"image/color"
)
// average convert the sums to averages and returns the result.
func average(sum []uint64, w, h int, n uint64) *image.RGBA {
ret := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
index := 4 * (y*w + x)
pix := ret.Pix[y*ret.Stride+x*4:]
pix[0] = uint8(sum[index+0] / n)
pix[1] = uint8(sum[index+1] / n)
pix[2] = uint8(sum[index+2] / n)
pix[3] = uint8(sum[index+3] / n)
}
}
return ret
}
// ResizeRGBA returns a scaled copy of the RGBA image slice r of m.
// The returned image has width w and height h.
func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA {
ww, hh := uint64(w), uint64(h)
dx, dy := uint64(r.Dx()), uint64(r.Dy())
// See comment in Resize.
n, sum := dx*dy, make([]uint64, 4*w*h)
for y := r.Min.Y; y < r.Max.Y; y++ {
pix := m.Pix[(y-r.Min.Y)*m.Stride:]
for x := r.Min.X; x < r.Max.X; x++ {
// Get the source pixel.
p := pix[(x-r.Min.X)*4:]
r64 := uint64(p[0])
g64 := uint64(p[1])
b64 := uint64(p[2])
a64 := uint64(p[3])
// Spread the source pixel over 1 or more destination rows.
py := uint64(y) * hh
for remy := hh; remy > 0; {
qy := dy - (py % dy)
if qy > remy {
qy = remy
}
// Spread the source pixel over 1 or more destination columns.
px := uint64(x) * ww
index := 4 * ((py/dy)*ww + (px / dx))
for remx := ww; remx > 0; {
qx := dx - (px % dx)
if qx > remx {
qx = remx
}
qxy := qx * qy
sum[index+0] += r64 * qxy
sum[index+1] += g64 * qxy
sum[index+2] += b64 * qxy
sum[index+3] += a64 * qxy
index += 4
px += qx
remx -= qx
}
py += qy
remy -= qy
}
}
}
return average(sum, w, h, (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
}
+2 -2
View File
@@ -18,7 +18,7 @@ func init() {
bs, _ = ioutil.ReadAll(gr)
Assets["angular.min.js"] = bs
bs, _ = hex.DecodeString("1f8b080000096e8800ffd41c5d73db36f23dbf0255d3924a64cae9dd746eac389dd6497bbe7c79e2242fae1f2011921853a00a82b635aefefb2d3e48020448d176d2a69eb98b0d2cf61bbb8b05d8f1a34f799a508ea62cbbca093b409c1564846619e5092d48f9f73a2d72f13ff5377a347e307eb448b3294ed1c30334c7690e40982e8a1433fdb7007a1014f04bce5932e3c1e4c1834bcc50bea133be4ce8021d962ba25516172909836a2e18a1b3f3e1442e28583ac580e610058ce4124f051709465996a68485c169397ac4590a18e605fc9d6414850ff359b6060e1f2e395f0fd1cd03043f02f79a91cbe7980be4fb936a7441f8db97302464ad470571ccb8e25c8a089c8849855db04289249803c0cd76d2989c270b777cb5397e2e240bac519ac54420393bf72039a642508b3d3d4f18cb98675d4e087d21e65c4aa07992ba6c31b2ce4a29e4f8788c4ec18c7491a32999678ca06996a5394ab3ec02463827cca6c8b904065ee4f84d121fa0e05592734201115807249c81af48aba11396f16c96a54841a09fe318d49d931c00f9660d7e187072cde12f6d05e588db9181fc35be3e25347e395de706fab7055f64c266ef84995f25ab84a3f065f2cb381fd6b869b19a12d689fd1de0c3f49882a097383d3528a819544ea1f0d698b5e7f891ebc9bbe33fc10cc30649df913f0a003295032a43a020584963a9a31aa43f7e4072b4849d4c1a9a17c87f4d5282d4ac32407fd51b147e9391e6674a5f503c4d496c10515308e632d8eda4c62bdcb393ed57d9ac05a79cb90bca5331fe8b8aa4a6878b61548f9be8acf51f4ee889cb8e1a4162d2bff8dc8e438b2239f56f3fbdab4cf57d38becf96fb600b2ab0fd5cf02581e431c332f47eb044de89f004e7f955c6e26ea4069446bcae473ab97dffcadc5c3080fefbfefdc92982888680502f5b97eaaed20ba48bd3623623242671586617f193cc51f88d4c26e6a861aa84263c1c4eeca930f896120ed25cc8981d0c4590c669182c9398040d683755899fadc5429db86ec946bdb09b095f66b459d9ba2afb15436c70f5d5ce2c23bc60b44d48bf9adb747933c5b38b98656b3035d003cf02635f90cd34c32cd605ccb645d55d0256e9730e722c0570558398bcc93a2402846159dc3c46c138dfc0465c018fb9f0a63c0febc531e6b8299ced76139f6115466043ac37581eaa5221f473a791970632d7d5bfbf9d7e82ac1481cef2d02c1a8611eca51778b6349027b16318af026431f293407318c000a13318f8f0eef8285bad330a014060eaab1e430d12ef59129f3b9a684a65feeee7d1a8f27a5b4a548f34bb02eaa2d684eaee2a1c8e2c081ec3642860f6aa9a7488c6e8c9fefebe0d99c43afc943f46090bebbd6e9050c8cc7589db98cd0a5e4d5bf3222882c65142519b8265841393d112e76faf28d4716bc2f8461aca032f7ecab3c5c499dd3a239c6d5ab008a2c2a49570af315f462b7c1dee8fd07fd023650909714c7fd97092bfcf38a4f53d4fb9ee4009d5f378e8726851ae14d74e1a8aab3eb42db056e25b04f96fb644216953ada394fdbe2278005d6b58eef4f8b041cec5603b98b9400d35f6a1cf39ed4355338ceddaacea40d47b9f368f517e72dbc983e651092a3c5ee456b817c3cddc266a8b6c6e4725010771e9104e66058dc93ca150fcf9531f142cf402f6380dccec63517051836de0d000bbf81b41a215f329cfd66ba0ecc52c8fed5cc5984044660f19395faf2ec1e5bf67fbe711cf3e00017604a68164f3584d805da63967e193e1c496432f3f4403713c84626080fefc13d5a3c7714a064d59d434b8d90085839a4bd12c38216c06f9032f8836cc6334f86e38f04aab556208d44ceeebec288582f3cbdb3ba1f3ec0b183b16e731f6d96c6d192c48c034ad94f53e0cdaaab81a4daeecde8a69cd9215661b2f268ffa6c13da2ef1c5ed0859bc4d608f76555f4de603897fbf1f56cb82eb1907a90002b291d74dc4b65224c63e0083874953ad32dbcdd30cea4720e30f8aa281e5098a62f8686e55f6825b11e401ce931cf582e80dfc7bfcfcdcf63901d8d44d391e899a312592ac5022a8c297344b4f39a6486824686424444423b37d990e4e28141ba5491612d077c326c68e80133c4f722d7b15883d6a3d9e0991fe094acd2e6eab4f7d6eba8dd256504ae6edda7203f557ab2e3736f6d2991b0877eacc8c8baeca4423e82fd698664c2a4df7a1bae2fa4f1df63654feb789d008029d39aa5d948f64a719043336a7d2d18cbb85163635c4256139e0f6f1f725d5932690783fb6d3de6167c8b5b120ec68c7165748b0120726799562deac00869413b3fd21f82dc9d25a9548619d58470d21a2c61ba5842ef812ca2df4a445e2aa38e81054638332b9d519dee0952b6f9737007c0b4726480753b689cb4a1d4eb83ffa533e5f26f91b1fa74d9b5065bac6b2b0a1615d7329e05e55d6202cd4f1482eb28bfba68a76e9a78f727a6b86c409373af22d2dc130f8b6bc35ebdba6944de9a1b7c0c597643749efad62a3bd6901456fd772ff47eace40876a2b44b850a79c45f93a4d78188c8464786d6cbc6b63e35d471c72193882e764bfce72a70f07a4401dff3b7dfb26cae5dd6432df841627c311ba59121c43a8394037c15146811fbef71e7c2b0095e235b0a5ee14c69ff28c065b8bb0cf2203d1ff1eb4750264ebba55df569bdc6ed8b774db6fdba66ed195c66cb6ed77dd283744cb97058fc5d6ba8b6c7ea64a94deee8caf916f2cb89b76b677975f6c616fca6984e0126dc118389a5ef13022d7e0777178b31d9561d565459000f5bdb8864de355a305764ad2b96a1937b2bf95fc5d796bc6a2eac20f36283aace27d351c7dca120a7b1679fc4600bf004e32163d843d72c224d3561017262bd576a780665b00c76ecedfa9f91b534620196f285e01cd6d0fedfbe3a0ad7e3fcc5faf1c38b8134ebaf5e3a3e8dce6c97ebe5f25bbafe37c7ae8577a79d370e9d3dfd44e6dbaaf9aedb1bb2564930fa3df28af394461a2ae3ad42d96bf332ce76403bb81b331d143ca7b48da94d6d5fc8e94fe7764d466428102a5d35fcb4a1142d208c5192523944c6e2b682f9fd754904ff70e5415689bf53178408a67241ca331301c00b96a64af1cd1e58d83b38a52be307cc7f2a9222274e7aa46ddedc98b1f94a0a7f61e55a71a9878fcd8d7db3061cf9273f3e4692ba5e34ab65c5c4b3c714035e7761a2c7fa68ce08b1e3d0f754309985ab6b492785de4cbd04dcd2d312dca33c6c3b2dd8019b96be8f91a7662c69784957cb6157766d3a0358ef70ee3a236710377cb79b233422450503543f91dbc9a364c048e3971dc9ef66cb198c2b73ba847629ca6dd762863a2e151b5f59a81453bb573cc36c08cc36cee2f7bc54da838c5f5740d7573eafa46db519b44ef931541cf9c87aa3bbd639612cc5e9417b59d25a1f9fad5e2f3cce65af772f6d09373c9d6ce8429d78d2527819fLine truncated
bs, _ = hex.DecodeString("1f8b080000096e8800ffd43c5973dbb6baeff915a89a965422534eef9dce1d2b4ea775d25e9f6c9e38c98beb074a8424c414a880a06d8dabff7e3e2c240102a4683b6953cf9c531bcbb7e3db0066fce8539e12cad194655739660788b3028fd02ca39cd002977fafd32217ff537fa347e307e3478b349bc6297a7880e6719ac3a2982e8a3466fa6fb1e84150c02f396764c683c98307973143f986cef892d0053a2c7744ab2c29521c06d55c304267e7c389dc50b0741a03984314309c4b38d5ba4810cab234c52c0c4ecbd123ce5280302fe06f9251143ecc67d91a287cb8e47c3d44370f10fc08d86b862f9fc75c00df9f54a30bccdfbe8421c16b3d2a90c78c2bca258b40899854d00529144b84392cb8d94e1a9373b270c7579be3e782b3c01aa559820590b3730f90632a18b5c8d3f398b18c79f6e518d31762cec50492c7a94b16c3ebace4428e8fc7e814d44817399ae279c6309a66599aa334cb2e608473cc6c8c9ccbc5408b1cbf21c9010a5e919c630a80403bc0e10c6c456a0d9db08c67b32c456a05fa354940dc39ce6121dfacc10e038eaf39fca5b5a00c713b3280bf8eaf4f314d5e4ed7b901fe6dc11799d0d93ba1e6576445380a5f92dfc6f9b0864d8bd514b34ee8ef005e4c8f29307a19a7a706063583ca2914de1ab2b61c3f703d7977f827318be180a4eff0e7021699c20191211010eca4899451bda43f7c0072b484938c1b9217c07f2729466a5629a0bfe80d0c7f484ff32ba52f683c4d716220515308e63238edb8862bccb393ec57d9ac05a69cb90bc85331fe9bf2a4a6858b61548f9be0acfd1f4ee8894b8e1a4162d2bff9dcf6438b829cfa8f9f3e55a6f83e1cdfe7c87db01915d07e2df81243f098c5d2f57eb058de09f024cef3ab8c25dd408d551af0ba1ee9a4f6fd2bf370c100fafff7ef4f4e11783404887ae9ba1477155e205c9c16b319c6094ec232ba881f3247e177329898a386aa08253c1c4eeca930f89e620edc5c489f1d0c85938ed330589204078dd56ea8123f5b8b843a70dd928c7a633711bec86893b27545f67b0cbec195573bb10cf382d13626fd626e93e5cd349e5d242c5b83aa011f581628fb026fa659cc129dc06c5b44ddc560153ee7c0c7522cae7210933699874400302c939bc72818e71b38882ba03117d694e761bd398979dc64ce36bb894fb10a229021f61b240f55aa10faa9d3c04b0599fbeadfdf4e3f41548a40667968260dc308ced28b78b6348093c4518c57003219f94580390c6000d3190c7c78777c94add61905072020f5158f210609f78c24e78e249a5c99bffb6934b2bcde9a12d923cdae00bbc83521bbbb0a87236b054f6032146bf6aa9c7488c6e8c9fefebebd9224dafd943f460a0bfbbd66402844e63ac56dcc6605afa6ad79e11441e28850d42660e9e1c464b48cf3b75714f2b835667c2315e5592f7ecada62e2cc6e9d11ce362d500452a1d28ab9d7315f46abf83adc1fa1ff438f9426e48a63fadb86e3fc7dc621acef79d2756795103d4f862e8516e64a70eda821b9ea83db5ad68a7c8b20fecd9628c46da27584b2df9705cf42571b96393d3e6ca07321d806666e50438d73e8334ebba86abab15d87551544bdcf69b38cf2a3db4e1e344b25c8f078915bee5e0c37639bc82db2b9ed95c43af04b8750991534c1734221f9f3873e4858e8059c711a98d1c7c2e08206dd40d100a7f83b81a215f229cfd66bc0ec852ccb76ae7c4c203cb3078d9caf7797cbe57fcff6cf239e7d0004ec085403c1e6b19a00bd4c73cec227c389cd87de7e8806a23c84646080fefa0bd5a3c7498a074d5ed43498d90085839a4ad12c38c16c06f1235e60ad98c768f0c370e0e5568bc460a819dcd7d9510a09e7d7d737a1f3ec2b283b11f518fb62bab6141610504d2b667d0e83b62cae06932bbdb7425a33b28ad9c60bc9233e5b85b6497c753d42146f63d8235dd55793f140c2dfef07d5d2e07ac6812b5801d1c86b26e258291463df0283864953ac32dacdd30cf24740e3778aa281e5718a62f8686e65f6825ae1e4619d2738ea0dd11bf8eff1f373dbe6c4c2a66ccaf148e48c29966885104114bea0595aca31454222412322212c1a99eddbb47342a138284db410807e18362176389ce039c935ef9523f688f5782658fa370835bbb8ad3c75dd741ba1ad2095ccdba5e53aea6f565cae6fec2533d711ee9499e9175d918946d0df2c314d98149aee4375f9f55f3af46d88fc1f63a1e1043a63543b2b1ff14e3508626c4aa5a119770b2d64ea159798e500db47dfd7144f4a20f07e6cc7bd43cf106b1381d8918ecdaee060250a26799562deac00849463b3fd21e82dd1d25a9448419d58a5866051c38d524c177c09e9167ad2c271951c7430aaa1419adc6a0c6fe295cb6f9735c0fa168acc251d44d92a2e3375a8707ff6877cbe24f91b1fa54d9d50a5bac6b6b021619d73a9c5bdb2ac4158a8f2486eb293fba68876c9a78f707a4b0627841b1df9969660187c5fde9af56d53caa6f4d09be0c69778374aefad62a3bd692d8adeaee5f98fd49d8176d5968b70579d7216e5eb94f0301809cee2b571f0ae8d83771d71886560089eca7e9de54e1f0e508138fe73faf64d94cbbb4932df841625c311ba59e238015773806e82a38c023d7cef3dd85600228dd74096ba53187fca331a6c2dc43e8d0c44ff7bd0d60990adeb56795b6d72bb61dfd26dbf6d9bba45561ab2d9b6df75a3dc602d5f163c1147eb2ebcf9892a417abb33be46beb1e16ed2d9de9d7f7184bd21a7e1824bb0056360687ac7c3085f83dd25e1cd7654ba5597148102c4f7e21a0e8d578cd6b2539cce55cbb811fdade0eff25b131655177e7040d161e5efabe1e85346289c59e4b11bb1f8055092b1e8219c91132689b69cb8505929b63b39345b03c40df94df74992cfc66d59beccae023fac38d901cca7c51b535e407eb2a1f10ae8dff6d0a4dfa7daaaf4aff9fb059d60485df14e613b189d9b417937e017c9eeab3d9f1cfaa571de905e9e8fefea03621e0535dbc353c8954d3a8cdea5bc3211498eba36513762fe2eb39c93cdf006ccc6440f2eefc169935b57f23bd2837f223a378313243b9df65a669de0de4628c9281e2132b92da3bd6c5e63413ed93bab2aa7ddccb5c102d27886c3311a03c101a0ab46f6ca119d2a39302b2fe573e9774cc52a244276ae68d43da1bc4442043db5cfa8aa9060e2f1635f9fc45c7b46cecd2ad6164ac7f56eb9b9e678e22cd594db21b5fc99321c5ff4e89fa8db4e80d472a415c7eb225f866e986ff169519e311e96ad8b98e1bbba9e6fe124667c895949675ba26836205afd786f372ef21cd771b7d4a69d1e824072d674e577b06ada501118e6c4317bdab35d6332df6ea01e8ee334edd643e9130d8baab5d7742cdaa89d92dd586614c6b93f8516b7aaa222ec691aea16d6b58db6b21d47efc90aa367cea3d79dd6314b71cc5e9497be9d29a1f992d6a2f3Line truncated
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("1f8b080000096e8800ffec5ceb73dc3692ffeebf02e6eef95127cec88fec6dc9a3a9726467e3daf851567cb9ad543e6048cc101109300028794ed6feedd70df0fd98a1c623d9b9e483ad211e0d74a3bbf16bb0c1d9dd176f4f7efcd7bb972432493cbf33c33f24a66275ec31e111b1f2699a1e7b7a2d021371b1b245811446c93866ead83b2d6a4e8c8a3d12c454eb630f1bc5929e794892d1707e879059c20c2541449566e6d8cbccd2ffbb575544c6a43efb2de3e7c7defff81f9efb273249a9e18b980159189109e8f5eae5310b57acd64fd0841d7be79c5da452995ad30b1e9ae83864e73c60be7d38205c70c369eceb80c6ecf8d1e4b04328643a503c355c8a1aad4e339a9948aa4e8b988b33a2580c12836a136486f00029458a2d8fbd253dc7c7490a829cdfc10e869b98cd4b21924fe4f2127ee93732646f60a4070fafae6653d7aa1cc0115b4869b451349d065a4fcba749c2c5044abc7c1e661d331d3166dc0ced3331eb149830eca3c1ceb68690850cd7e4d2fe2424a5610813f217d218991c91ff3a4c3f3ecbeb96c0b4bfa4098fd747c4fb9ec5e7ccf08092372c63de01290b0ec87305d23e209a0aed6ba6f8d291b8ba63ff64f17f46df94232654adb8f08d4c8fc8a3c9372c69b49de064fd440aa9531ab0b257632eaf9988e501792d050de0ef89145a82421e10ef44668a330553bc801996645a43505036d4ee9009cdc2b9b1aa6bd41c55a75b8bf21aae5dc2820cd79694c38d947b6b4bca612985855421534e76428a165fb15cc996988fc8e1b3e64ad74a2c19ff9b6ac153a9395ac411ea14d8e4797b00ae8d2fa4bfc8c0299872285b6c15ce478573536b7504c6e22c11659f90eb34a6b0965c80ae337f11cbe0ac9807a8b6b364d0c7423f4ac5517c1519509daa624183b3959299087114a960faabc583c74ffe76401e3f3dc4ff1e3d7cd694a0a221cff411795263be90cfa3f423795a9517827c0cc58f8be2ab365fa066621252701c2d3d8fd912667b58297a83bd478755b1d57c1af315ca1f997cb65d5685800b47dc952fae05b9cb13f49a54981e4b20266a9ad90573227e7a78d8db3cec780fc7e463f01d3d4381379a5aed804d62ea76893b33abfad635def57df2a34c611515f17d5726e879b9c5d073ac717f50f38b9f215bd22c869d00b62866dbf115b5fedcb9b959c84b22e8be29884ee575e82061c19a63f80b4545e8cd673c59153568521ed12a400febe393ffe8f1dfad63276ee3f19e3c06b76f25e67e4fe7a4f4f3339dd0382e88453c04dbf63f82236e8eef7c5e66188cfe09840555f3de0dc2929be72d0a4eb2b8c647219dda4fab4b15e3312f9a874aa6a1bc10659dada7f9bef317afdd0ea4bf5ae1368d7a9e3fd4a9bc0cb9b927163a7d365b9492a70ab7a4d97401ff6863a06adee500091359633676be73989355701e9cc1961c86ef592a1f3c04aa7521aee2751ae1b64bca5f7e14e27a3a71dd6309ccec791812ec0e8e4eaa354e69368511b60e89eb306a4860f78239965bc32289c1014b51f0731ea29e8e981603799f326340d1f4a8b90572d59a57d17daff3d25166703947cd492e97ed39e5dd472e8e62da5065462ece129a47ad01df3b0a7de3cda6595c3d376babbad914c46361d7146cceb9b001efe3daa3cffb8e2b6dc0795d1c1029e23501247921085f12c102a63555eb67249f19b9a04a206cccfd634e1e64c0c150ef02fd255fbd12e8764aa305c295cd372613fb49085eac6667f57a100b8b89fddfcf876dfa87765b1f5dba6d358b9e346b2c9ef5e605176f180b59087bc0937929b161b2b843b47d413aff3162c4f19b29ebed4944355930065b303d672159001617d2101a007ea1e05227953b26498622cfa76364d908b6400672bf68929ecca6695319b64e1a315b6d97c99bc19c0cccd4a171f750aed3c20802ff7c9dd83ff9ae46528058b9e7be195d77d3684db4ae2731a36ac93f7a3d6bd52c683cd61ef29f1d8d67ab2c861d1d14b4a5cf0dcd2de861c7ca615bac491e200c40ccf1b0a2d0abe57feb3501b7548818538ff010c59ad3e74c8f308bcb4bec7082150ff0d7e4d50bd89ded3ac123a3c65104b846f0ef0f30655cb17146546f35604fcdddbab11903df314d75b147a7b0fb62d4fa970687c5068f853933c00270d0a07c9dbd95e4545e70c502bb4e9f888d8cdf51135d5d0d91ad29f9dc513835d46475a936b18e53ab268c4077b2d5488b556eb0db146e213a52caf09afea9dec40573607f29c4a61047b53d8243d2f5c6a415ffe5cf10ebf3147161abbfc3cf84344b55bb080ba3edbec2d03630a899dcab17b32910e9a11c36006c6321ad84a15fd899e4b43bcbdd27be943186733265a2c5c077b6669799974a7c1b0c043201d0db868bb863454a0afebf7623da818b1e43ba695656b15cb01623ff80321a3710f7b558490033c73fe75cfc32595972dff19869f03034bea06bfd264b164c5d5d9125161f90814edfae8dedb4e082e2d27e7bf3028964d296c70f32d8a33862a4764d69d83e5f4018412cb3d0475c1f4bda8ecade0268934b0bd43e4f220220e63505825dbe803cf00ca72585d7540378dc553bbaedec14f220c1b2fb1e40c65b8835bcf9bf98eeeeac3dddeeb6fabd9183dd6e5c623a0234e3d3b8e32cb13c243f7113ed4960058a1305849b60ecae7bba5c5e8ae28066c9853b2510b635385df4ba0d61fe152601031f9061217e8e70a1a40b0ba0108144bbb00d9286601900ccfe4025553c015b29c0a4d73a97b0473428ba31a10aecde018f5bcb8aa7492e24ef22c0dd0292664892c716b856b5a8c2b2bda7b042b49466289e284f32ebaa077d4f962b54c09f8b73c0070f7fd9218a1815478c8f241c534508814fbe3302986daef818495c6f73acdb514ecb4216da56dbb1587f685e5b203f88fb2b46fd43b87fc0a98ec5fe9d2382e7afc907c3e38de073c396acd7b08d2513f8b36d47eddf213e931daacf748ba393771ff6c1519066ef980ac00e5ae8021e05c06d45e3a3475757ff715bac6e84542ff262f29e1a766d8eb958a4b87c09034d0caeaee0e976b9cad21e9e3ea4bb732433d361a9775fddc053b191e7fa00f49f0b213311b0b7ff24778f4906560c30a0c77047738faf5623a9dad16c310e3965ea7c28ac1d8f745a1bbe7d6f693be82cc0c3676f13a7defcad7d0f39045606898754ac982a69df1d20be5c6ea17e6b9ea47d28f2df4ce95dfcc7b9ebd71f840fc0e4ddb1dc0da039876bf38df98b01ba1107b3768ef660b6821143b84a9a88290bec773a9dfd32b86a789dc7bc8c6cf4eec55ce3f4c875cdcf98ea80ad3bcaed83b83f1c82c364b1eefb6660425fdb53a1acb16f63516f69dfdffd24740b433d7afabb0768405eb00045a27f6e1ac82f93ff7fe06d13b7fb00765f014640b6a0ef4e5afa2758d872fa73a7faed4e7da6cdb7cf58f45229a9747f8a05c33af71e771233b132119993c3af31d7e28dLine truncated
bs, _ = hex.DecodeString("1f8b080000096e8800ffec5de973dc36b2ffeebf02e6eef3514fe4c847f66dc9a3a9722467e3daf8282b7e795ba97cc09098212292a00150f23c59fbb76f37c0fb98e18c478ab29b0fb186381ae84677e3d7608399de3f7d77f2e33fdebf22a18ea3d9bd29fe21114d96c70e4b1c922c5d9aa6c78e5a25be0e79b23445be48b41451c4e4b17356d49c681939c48fa852c70e368a043d779024a3c1ec1e21d398694afc904ac5f4b193e985fb57a7aa08b54e5df629e317c7ceffb91f5fba27224ea9e6f3880159189125d0ebf5ab63162c59ad5f426376ec5c7076990aa96b4d2f79a0c3e3805d709fb9e6e180f0846b4e2357f93462c74fbcc30ea180295ff2547391d468759ad14c8742765a443c392792452031a8d67ea609f7915228d9e2d859d00b7cf45210e4ec1e76d05c476c560a917c215757f04bbd15017b0b233d7a7c7d3d9dd856e50096d85c08adb4a4e9c4576a523e79314f3c2871f279e855c454c898b63334cf44af526042b3cf1a3b9b1a42e62258912bf393909406014cc89d0bad457c44fee730fdfc22af5b00d3ee82c63c5a1d11e77b165d30cd7d4adeb28c3907a42c38202f2548fb80289a285731c91796c4f53df3278bfe3bfca61c31a672c913578bf4883cf1be6171a3ad87937563910895529f95bd1a7379c392481c903722a13efc3d118912a09007c4391199e44cc2142f61862599d61014940db53b608962c14c1bd5d57286aad3ad45790dd72e6041866b4bcac15acabdb525e5a094c25cc880492bbb44242dbe22b1142d311f91c317cd95ae951832ee37d582a74271b48823d429b0c98bf6005c693711ee3c03a7a0cba14cb151381715ce4eadd511188bb23829fb045ca51185b5e409e83a73e791f0cf8b79806a5b4b067d2cf4a3541cc997a106d5a92ae6d43f5f4a9125018e22244c7f397ff4f4d95f0ec8d3e787f8cf93c72f9a129434e0993a22cf6acc17f279927e26cfabf242904fa1f869517cdde60bd42cf1020a8ea3a5e7115bc06c0f2b456fb0f7e4b02a369a4f23be44f923932f36cbaa1070e188bbf2c5b520f7798c5e9326bac712880e9b6676c9ac889f1f1ef6360f3adec332f9147c47cf50e08d26463b609398d85de2ded4a8be718df75d97fc28525845495cd79625f4a2dc62e805d6d83fa8f9c5cf802d6816c14e005b1433edf8921a7f6edddc34e0251174df144427f33a7490b060cd31dcb9a449e0cca63c5e163568520e51d2470febe293fbe4e95f8d632776e3719e3d05b76f24667f4f66a4f4f35315d3282a88853c00db763f83236e8e6f7d5ea6198cfe05840555b3de0dc2909be52d0a4eb2a8c647219dda4fa34b15e3112f9a0752a481b84cca3a534ff37de74f4ebb1d487fb9c46d1af53c7fa853791570fd2099abf4c5745e4a9e4adc92a69339fc471b0355f32e07885992356663e63b2be7646149c4fd73d89a83e0034bc5a3c740bd2ecc65b44a43dc7e49f9cb0d035c572bb6072c8619be0c0282ddc1e109b9c2a94d2730d2e8a1715d460d0dec5f322b82d6f0486270e05234fc8207a8b75b4c8fc13a9c31ad4101d5a839fa62d99a5fd17d4bc1f0f172f9247d68da1e361497e4f5a91db5aee47b918b0a338d6a366a7e62b1e84cce76df522692294da51ea92c0b681eb606fe6029f48d3b9d6451f5dcacadeaa6131097818513f009d6c50e7847db1e7df2775c2a0dcef5f28088245a1140ba9709e10b92309f2945e5ea05c967462ea94c10d6e6fe3b270f32e0208cfb407fc197af13748ba55301c2954f6a4c2672e300bc6ccd0fd4eb412c2c22e65f371fb6e9bfda6d5ddc724cab69f8ac5963f0b6332bb878cb58c002d8a39ecd4a890d93c51dacedabd2d98f212396df4c9add8884549139630011e8050bc81c62854468427dc057145cbe576d1724ce50e4f974b4281bc116cd40ee974dd2de7492369561e3a41153d676c1bc19cc49c34c6db4601fca759aeb84c07fae8acd9f7cd7252940c07c67b9195db7d3684db4ae2711a372c13f3b3d6bd52c683cd61ef29f1d8d67cb2c02c4010adad2e786e616f4b063b591182c4c1e214c414cf4b8a2d0abe57fe93501bb5488685387f000c59ad3e74c8d308bab2bec7082158ff097f7fa14d08359277864545b8a002709fefd01a68c2b36ce88eaad06eca989261a6001f88e68aa0a0c91023ac0a8fa4f0d0e0b0082853933c00270d0a0bccd9e4f722aa75c32dfacd3176222f7f75487d7d743646b4a3eb314ce34d5595daadd6daa0d73d09d6c34d262951bec36855b888e9432dcd23fd59bd86013ec2f85d819e2bcb647b048bfde98b4e2d3fc5969c953c4adadfe16df13d22c95ed222c0c37fb0a4ddb00a56672881780480fe5a001b01b0b69240cfd82ce2427dd59ee3ef1858830dc14294b5a0c7c676a769979a9c4b7c1802f6200e56df88a3b562845c2ffdf6c443b70d1634837cdca3212f336e0fc1b94d1a811096cc54a0c1836fa39e7e2176f69c87dc723a6c0c3d0e892aed4db2c9e33797d4d16587c40063a7dbbd2a6d39c271497f6db9b174828e2b63c7e10fe1ec51121b52da561fafc06c2f02391052ee2fb48d076b4f80e409b5818a0f675124900626e2910ecf21bc803cf985a5278431580c75db5a3dbce4c210f120cbb1f0064bc8358c399fd83a9be00b0d3ed7eabdf5b31d8edc625a64240332e8d3ace12cb03f213d7e19e0456a0b8a480701e86ddaaa7cbd555521c202d7862a3f3c4b406a78b5eb721cc3fc32460e003322cc4af112e947461011422906817b641d2102c0380d91fa8a492c7602bcec0f988393a42d18d095560f7f679d45a563cedea3fa8d83520698624796c816b558b2a0cdb7b0a2b9296d20cc513e5496b5df5a0efc962890af873714ef9e8f12f3b4411a3e288f1918465aa0821f0c9b54600b3cd151f2389ed36c7ba1de5b40c64a16db51d8bf587e6b501f283b8ef30ea1fc2fd034e752cf6ef1c11bc7c433e6a1ead059f6bb664b5826d2cf6e0cfa61db57f87f84a76a83a572d8e4ede7fdc07477e9abd67d2073b68a10b784c006e4b1a1d3db9befeafdb62752da43acd8bc907aad9d61cf3649ee2f2c50c34d1bfbe86a7dbe52a4b7b78fa98eece91c87487a5de7d750d4fc5469eeb03d07f9924224b7cf6eeefe4fe31c9c08a0106f418ee68eef1d56f28643b9a2dc621674c5e0c85b5e3914e6bc337ef554d0795f978f8ecace3d499bd33ef4987c0ca20f180264b264bdaf707882f161ba8df9a27691f8afc2f936a17ff7161fbf507e1033079772c770368cee2da7c63fecd00dd888359334773305bc188215c2574c8a401f63b9dcefe36b86a789dc7bc1c6df4eec55ce3f4c876cdcf98ea80ade79de2ad83b8ff380487c96cddf7dfc084dada53a1acb16f63516f69dfdffd247403433d7afabb0768403e613e8a44fddc34905fbc7f3ff0b68edb7d00bb3b8011902de8bb9396fe0116369cfedcab7edb539f49f3ed3316bd925248d59fLine truncated
gr, _ = gzip.NewReader(bytes.NewBuffer(bs))
bs, _ = ioutil.ReadAll(gr)
Assets["index.html"] = bs
+13
View File
@@ -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() {
+4
View File
@@ -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;
+33 -13
View File
@@ -80,13 +80,14 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit&nbsp;<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span>&emsp;Add Repository</a></li>
<li><a ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span>&emsp;Add Node</a></li>
<li><a href="" ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span>&emsp;Add Repository</a></li>
<li><a href="" ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span>&emsp;Add Node</a></li>
<li class="divider"></li>
<li><a ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span>&emsp;Settings</a></li>
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span>&emsp;Settings</a></li>
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span>&emsp;Show ID</a></span>
<li class="divider"></li>
<li><a ng-click="shutdown()"><span class="glyphicon glyphicon-off"></span>&emsp;Shutdown</a></li>
<li><a ng-click="restart()"><span class="glyphicon glyphicon-refresh"></span>&emsp;Restart</a></li>
<li><a href="" ng-click="shutdown()"><span class="glyphicon glyphicon-off"></span>&emsp;Shutdown</a></li>
<li><a href="" ng-click="restart()"><span class="glyphicon glyphicon-refresh"></span>&emsp;Restart</a></li>
</ul>
</li>
</ul>
@@ -373,13 +374,36 @@
</div>
</div>
<!-- ID modal -->
<div id="idqr" class="modal fade">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<span class="glyphicon glyphicon-qrcode"></span>
Node Identification &mdash; {{nodeName(thisNode())}}
</h4>
</div>
<div class="modal-body">
<div class="well well-sm text-monospace text-center">
{{myID | chunkID}}
</div>
<img class="center-block img-thumbnail" src="qr/{{myID | chunkID}}"/>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;Close</button>
</div>
</div>
</div>
</div>
<!-- Node editor modal -->
<div id="editNode" class="modal fade">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 ng-show="!editingExisting" class="modal-title">Add Node</h4>
<h4 ng-show="editingExisting" class="modal-title">Edit Node</h4>
</div>
@@ -390,7 +414,9 @@
<input ng-if="!editingExisting" name="nodeID" id="nodeID" class="form-control text-monospace" type="text" ng-model="currentNode.NodeID" required></input>
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentNode.NodeID | chunkID}}</div>
<p class="help-block">
<span ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">The node ID to enter here can be found in the "Add Node" dialog on the other node. Spaces and dashes are optional (ignored).</span>
<span ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">The node ID to enter here can be found in the "Edit > Show ID" dialog on the other node. Spaces and dashes are optional (ignored).
<span ng-show="!editingExisting">When adding a new node, keep in mind that <em>this node</em> must be added on the other side too.</span>
</span>
<span ng-if="nodeEditor.nodeID.$error.required && nodeEditor.nodeID.$dirty">The node ID cannot be blank.</span>
</p>
</div>
@@ -405,10 +431,6 @@
<p class="help-block">Enter comma separated <span class="text-monospace">ip:port</span> addresses or <span class="text-monospace">dynamic</span> to perform automatic discovery of the address.</p>
</div>
</form>
<div ng-show="!editingExisting">
When adding a new node, keep in mind that <em>this node</em> must be added on the other side too. The Node ID of this node is:
<div class="well well-sm text-monospace">{{myID | chunkID}}</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" ng-click="saveNode()" ng-disabled="nodeEditor.$invalid"><span class="glyphicon glyphicon-ok"></span>&emsp;Save</button>
@@ -425,7 +447,6 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 ng-show="!editingExisting" class="modal-title">Add Repository</h4>
<h4 ng-show="editingExisting" class="modal-title">Edit Repository</h4>
</div>
@@ -485,7 +506,6 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title"> Settings</h4>
</div>
<div class="modal-body">