PortRelay:把内网服务临时、可控地开放给同事
边缘网关 + Agent 的端口中继:Agent 主动连出到网关,把内网目标挂到网关端口上; 通过邀请链接按设备授权,可选审批,首次连接起计时,到期、闲置自动收回。 - portrelay-server:边缘网关(端口池、白名单与授权租约、邀请与授权网页) - portrelay-agent:Agent(yamux 隧道、本地 API 与网页面板、流量统计) - mac-native:SwiftUI 菜单栏 App
This commit is contained in:
commit
86e159b3f0
67 files changed
+13888
No files matched your search
@@ -0,0 +1,260 @@
|
||||
// mkgif 把逐帧 PNG 合成为循环播放的 GIF,用于 README 动图。
|
||||
//
|
||||
// go run ./tools/ui-snapshots/mkgif -o docs/images/live-traffic.gif tools/ui-snapshots/out/gif-*.png
|
||||
//
|
||||
// 所有帧共用一套中位切分调色板;第二帧起只编码与上一帧不同的区域,
|
||||
// 区域内未变化的像素写成透明色,界面类动图体积因此很小。
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/gif"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
func main() {
|
||||
out := flag.String("o", "out.gif", "output file")
|
||||
delay := flag.Int("delay", 40, "frame delay in 1/100 s")
|
||||
hold := flag.Int("hold", 120, "extra delay on the last frame in 1/100 s")
|
||||
shrink := flag.Int("shrink", 1, "downscale factor (box filter)")
|
||||
flag.Parse()
|
||||
if flag.NArg() == 0 {
|
||||
log.Fatal("no input frames")
|
||||
}
|
||||
|
||||
var frames []*image.RGBA
|
||||
for _, path := range flag.Args() {
|
||||
frames = append(frames, downscale(load(path), *shrink))
|
||||
}
|
||||
bounds := frames[0].Bounds()
|
||||
|
||||
pal := medianCut(histogram(frames), 255)
|
||||
transparent := uint8(len(pal))
|
||||
pal = append(pal, color.RGBA{})
|
||||
|
||||
idx := newMapper(pal[:transparent])
|
||||
anim := &gif.GIF{LoopCount: 0, Config: image.Config{ColorModel: pal, Width: bounds.Dx(), Height: bounds.Dy()}}
|
||||
var prev *image.Paletted
|
||||
for i, f := range frames {
|
||||
cur := image.NewPaletted(bounds, pal)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
cur.SetColorIndex(x, y, idx.index(f.RGBAAt(x, y)))
|
||||
}
|
||||
}
|
||||
frame := cur
|
||||
if prev != nil {
|
||||
frame = diff(prev, cur, transparent)
|
||||
}
|
||||
d := *delay
|
||||
if i == len(frames)-1 {
|
||||
d += *hold
|
||||
}
|
||||
anim.Image = append(anim.Image, frame)
|
||||
anim.Delay = append(anim.Delay, d)
|
||||
anim.Disposal = append(anim.Disposal, gif.DisposalNone)
|
||||
prev = cur
|
||||
}
|
||||
|
||||
w, err := os.Create(*out)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := gif.EncodeAll(w, anim); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
st, _ := os.Stat(*out)
|
||||
log.Printf("%s: %d frames, %dx%d, %d KB", *out, len(frames), bounds.Dx(), bounds.Dy(), st.Size()/1024)
|
||||
}
|
||||
|
||||
func load(path string) *image.RGBA {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
img, err := png.Decode(f)
|
||||
if err != nil {
|
||||
log.Fatalf("%s: %v", path, err)
|
||||
}
|
||||
b := img.Bounds()
|
||||
rgba := image.NewRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
||||
for y := 0; y < b.Dy(); y++ {
|
||||
for x := 0; x < b.Dx(); x++ {
|
||||
rgba.Set(x, y, img.At(b.Min.X+x, b.Min.Y+y))
|
||||
}
|
||||
}
|
||||
return rgba
|
||||
}
|
||||
|
||||
func downscale(src *image.RGBA, k int) *image.RGBA {
|
||||
if k <= 1 {
|
||||
return src
|
||||
}
|
||||
b := src.Bounds()
|
||||
dst := image.NewRGBA(image.Rect(0, 0, b.Dx()/k, b.Dy()/k))
|
||||
for y := 0; y < dst.Bounds().Dy(); y++ {
|
||||
for x := 0; x < dst.Bounds().Dx(); x++ {
|
||||
var r, g, bl, a int
|
||||
for dy := 0; dy < k; dy++ {
|
||||
for dx := 0; dx < k; dx++ {
|
||||
c := src.RGBAAt(x*k+dx, y*k+dy)
|
||||
r, g, bl, a = r+int(c.R), g+int(c.G), bl+int(c.B), a+int(c.A)
|
||||
}
|
||||
}
|
||||
n := k * k
|
||||
dst.SetRGBA(x, y, color.RGBA{uint8(r / n), uint8(g / n), uint8(bl / n), uint8(a / n)})
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
type bucket struct {
|
||||
c [3]uint8
|
||||
count int
|
||||
}
|
||||
|
||||
func histogram(frames []*image.RGBA) []bucket {
|
||||
counts := map[[3]uint8]int{}
|
||||
for _, f := range frames {
|
||||
for i := 0; i+3 < len(f.Pix); i += 4 {
|
||||
counts[[3]uint8{f.Pix[i], f.Pix[i+1], f.Pix[i+2]}]++
|
||||
}
|
||||
}
|
||||
out := make([]bucket, 0, len(counts))
|
||||
for c, n := range counts {
|
||||
out = append(out, bucket{c, n})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// medianCut 反复把"像素数 × 色彩跨度"最大的盒子沿最长轴按像素中位数切开。
|
||||
func medianCut(colors []bucket, n int) color.Palette {
|
||||
boxes := [][]bucket{colors}
|
||||
for len(boxes) < n {
|
||||
best, bestScore, bestAxis := -1, 0, 0
|
||||
for i, b := range boxes {
|
||||
if len(b) < 2 {
|
||||
continue
|
||||
}
|
||||
axis, span := longestAxis(b)
|
||||
total := 0
|
||||
for _, c := range b {
|
||||
total += c.count
|
||||
}
|
||||
if score := span * total; score > bestScore {
|
||||
best, bestScore, bestAxis = i, score, axis
|
||||
}
|
||||
}
|
||||
if best < 0 {
|
||||
break
|
||||
}
|
||||
b := boxes[best]
|
||||
sort.Slice(b, func(i, j int) bool { return b[i].c[bestAxis] < b[j].c[bestAxis] })
|
||||
total := 0
|
||||
for _, c := range b {
|
||||
total += c.count
|
||||
}
|
||||
cut, acc := 1, 0
|
||||
for i, c := range b {
|
||||
acc += c.count
|
||||
if acc*2 >= total {
|
||||
cut = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
if cut >= len(b) {
|
||||
cut = len(b) - 1
|
||||
}
|
||||
boxes[best] = b[:cut]
|
||||
boxes = append(boxes, b[cut:])
|
||||
}
|
||||
pal := make(color.Palette, 0, len(boxes))
|
||||
for _, b := range boxes {
|
||||
var r, g, bl, total int
|
||||
for _, c := range b {
|
||||
r += int(c.c[0]) * c.count
|
||||
g += int(c.c[1]) * c.count
|
||||
bl += int(c.c[2]) * c.count
|
||||
total += c.count
|
||||
}
|
||||
pal = append(pal, color.RGBA{uint8(r / total), uint8(g / total), uint8(bl / total), 255})
|
||||
}
|
||||
return pal
|
||||
}
|
||||
|
||||
func longestAxis(b []bucket) (axis, span int) {
|
||||
for a := 0; a < 3; a++ {
|
||||
lo, hi := 255, 0
|
||||
for _, c := range b {
|
||||
v := int(c.c[a])
|
||||
lo, hi = min(lo, v), max(hi, v)
|
||||
}
|
||||
if hi-lo > span {
|
||||
axis, span = a, hi-lo
|
||||
}
|
||||
}
|
||||
return axis, span
|
||||
}
|
||||
|
||||
type mapper struct {
|
||||
pal color.Palette
|
||||
cache map[color.RGBA]uint8
|
||||
}
|
||||
|
||||
func newMapper(pal color.Palette) *mapper {
|
||||
return &mapper{pal: pal, cache: map[color.RGBA]uint8{}}
|
||||
}
|
||||
|
||||
func (m *mapper) index(c color.RGBA) uint8 {
|
||||
c.A = 255
|
||||
if i, ok := m.cache[c]; ok {
|
||||
return i
|
||||
}
|
||||
best, bestD := 0, int(^uint(0)>>1)
|
||||
for i, p := range m.pal {
|
||||
q := p.(color.RGBA)
|
||||
dr, dg, db := int(c.R)-int(q.R), int(c.G)-int(q.G), int(c.B)-int(q.B)
|
||||
if d := 3*dr*dr + 4*dg*dg + 2*db*db; d < bestD {
|
||||
best, bestD = i, d
|
||||
}
|
||||
}
|
||||
m.cache[c] = uint8(best)
|
||||
return uint8(best)
|
||||
}
|
||||
|
||||
// diff 返回只覆盖变化区域的帧,区域内未变化的像素为透明色。
|
||||
func diff(prev, cur *image.Paletted, transparent uint8) *image.Paletted {
|
||||
b := cur.Bounds()
|
||||
minX, minY, maxX, maxY := b.Max.X, b.Max.Y, b.Min.X-1, b.Min.Y-1
|
||||
for y := b.Min.Y; y < b.Max.Y; y++ {
|
||||
for x := b.Min.X; x < b.Max.X; x++ {
|
||||
if prev.ColorIndexAt(x, y) != cur.ColorIndexAt(x, y) {
|
||||
minX, minY, maxX, maxY = min(minX, x), min(minY, y), max(maxX, x), max(maxY, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
if maxX < minX {
|
||||
minX, minY, maxX, maxY = 0, 0, 0, 0
|
||||
}
|
||||
r := image.Rect(minX, minY, maxX+1, maxY+1)
|
||||
out := image.NewPaletted(r, cur.Palette)
|
||||
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||
for x := r.Min.X; x < r.Max.X; x++ {
|
||||
if v := cur.ColorIndexAt(x, y); v != prev.ColorIndexAt(x, y) {
|
||||
out.SetColorIndex(x, y, v)
|
||||
} else {
|
||||
out.SetColorIndex(x, y, transparent)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in new issue
Block a user