package sprite import ( "bytes" "encoding/json" "fmt" "image" "image/png" "math" "os" "path/filepath" "strings" "github.com/pkg/errors" "github.com/srwiley/oksvg" "github.com/srwiley/rasterx" "golang.org/x/image/draw" ) type Sprite struct { name string infos []*ImageInfo Root *SpriteNode } type ImageInfo struct { Name string Path, Type string Width, Height float64 // image image.Image Fit *SpriteNode } type SpriteBuilder struct { outPath string sprites []Sprite } type SpriteNode struct { X float64 Y float64 Width float64 Height float64 Used bool Down *SpriteNode Right *SpriteNode } func NewSpriteBuilder(iconPath string, outPath string) (*SpriteBuilder, error) { basePath, err := os.Executable() if err != nil { return nil, errors.Errorf("could not get execute path: %v", err) } defaultOutPath := "style/sprite" if outPath == "" { outPath = defaultOutPath } if err := os.MkdirAll(filepath.Join(filepath.Dir(basePath), outPath), 0644); err != nil { return nil, err } files, err := os.ReadDir(iconPath) if err != nil { panic(err) } sb := &SpriteBuilder{ outPath: filepath.Join(filepath.Dir(basePath), outPath), sprites: []Sprite{}, } for _, file := range files { if !file.IsDir() { continue } subFiles, err := os.ReadDir(filepath.Join(iconPath, file.Name())) if err != nil { continue } imageInfos := []*ImageInfo{} for _, subFile := range subFiles { if subFile.IsDir() { continue } filePath := filepath.Join(iconPath, file.Name(), subFile.Name()) fileName := filepath.Base(filePath) fileType := filepath.Ext(filePath) if fileType != ".svg" && fileType != ".png" { continue } imageInfo := ImageInfo{ Name: strings.TrimSuffix(fileName, fileType), Type: fileType, Path: filePath, } imageInfos = append(imageInfos, &imageInfo) } sb.sprites = append(sb.sprites, Sprite{ name: file.Name(), infos: imageInfos, Root: nil, }) } return sb, nil } func (sb *SpriteBuilder) Build() error { for _, sprite := range sb.sprites { if _, err := os.Stat(filepath.Join(sb.outPath, sprite.name)); err != nil { err = os.MkdirAll(filepath.Join(sb.outPath, sprite.name), 0644) if err != nil { return err } } sprite.Fit() if err := sprite.Save(sb.outPath, 1); err != nil { return err } if err := sprite.Save(sb.outPath, 2); err != nil { return err } } return nil } func (s *Sprite) Fit() { if len(s.infos) == 0 { return } // 初始化根节点,使用第一个块的尺寸 firstBlock := s.infos[0] firstBlock.ReadInfo() s.Root = &SpriteNode{ X: 0, Y: 0, Width: firstBlock.Width, Height: firstBlock.Height, } for _, info := range s.infos { info.ReadInfo() node := s.findNode(s.Root, info.Width, info.Height) if node != nil { // 找到合适的位置,分割节点 fit := s.splitNode(node, info.Width, info.Height) info.Fit = fit } else { // 没有找到合适位置,扩展容器 fit := s.growNode(info.Width, info.Height) info.Fit = fit } } } // findNode 查找可以容纳指定尺寸的节点 func (s *Sprite) findNode(root *SpriteNode, width, height float64) *SpriteNode { if root.Used { // 如果当前节点已使用,递归查找子节点 if right := s.findNode(root.Right, width, height); right != nil { return right } if down := s.findNode(root.Down, width, height); down != nil { return down } return nil } else if width <= root.Width && height <= root.Height { // 当前节点可以容纳该块 return root } return nil } // splitNode 分割节点,创建新的子节点 func (s *Sprite) splitNode(node *SpriteNode, width, height float64) *SpriteNode { node.Used = true node.Down = &SpriteNode{ X: node.X, Y: node.Y + height, Width: node.Width, Height: node.Height - height, } node.Right = &SpriteNode{ X: node.X + width, Y: node.Y, Width: node.Width - width, Height: height, } return node } // growNode 扩展容器 func (s *Sprite) growNode(width, height float64) *SpriteNode { canGrowDown := width <= s.Root.Width canGrowRight := height <= s.Root.Height shouldGrowRight := canGrowRight && (s.Root.Height >= (s.Root.Width + width)) // 保持方形,优先向右扩展 shouldGrowDown := canGrowDown && (s.Root.Width >= (s.Root.Height + height)) // 保持方形,优先向下扩展 if shouldGrowRight { return s.growRight(width, height) } else if shouldGrowDown { return s.growDown(width, height) } else if canGrowRight { return s.growRight(width, height) } else if canGrowDown { return s.growDown(width, height) } return nil // 应该确保初始根节点尺寸合理以避免这种情况 } // growRight 向右扩展容器 func (s *Sprite) growRight(width, height float64) *SpriteNode { newRoot := &SpriteNode{ Used: true, X: 0, Y: 0, Width: s.Root.Width + width, Height: s.Root.Height, Down: s.Root, Right: &SpriteNode{ X: s.Root.Width, Y: 0, Width: width, Height: s.Root.Height, }, } s.Root = newRoot if node := s.findNode(s.Root, width, height); node != nil { return s.splitNode(node, width, height) } return nil } // growDown 向下扩展容器 func (s *Sprite) growDown(width, height float64) *SpriteNode { newRoot := &SpriteNode{ Used: true, X: 0, Y: 0, Width: s.Root.Width, Height: s.Root.Height + height, Down: &SpriteNode{ X: 0, Y: s.Root.Height, Width: s.Root.Width, Height: height, }, Right: s.Root, } s.Root = newRoot if node := s.findNode(s.Root, width, height); node != nil { return s.splitNode(node, width, height) } return nil } func (s *Sprite) Save(outPath string, pixelRatio int) error { img := image.NewRGBA(image.Rect(0, 0, int(s.Root.Width)*pixelRatio, int(s.Root.Height)*pixelRatio)) for _, info := range s.infos { // fmt.Println(info.Name, info.Fit.X, info.Fit.Y, info.Fit.Width, info.Fit.Height) // draw.Draw(img, info.image.Bounds().Add(image.Pt(int(info.Fit.X), int(info.Fit.Y))), info.image, image.Point{}, draw.Over) infoImg := info.ReadImg(pixelRatio) if infoImg == nil { continue } draw.Draw(img, infoImg.Bounds().Add(image.Pt(int(info.Fit.X)*pixelRatio, int(info.Fit.Y)*pixelRatio)), infoImg, image.Point{}, draw.Over) // if pixelRatio != 1 { // minX := info.image.Bounds().Min.X // minY := info.image.Bounds().Min.Y // maxX := info.image.Bounds().Max.X // maxY := info.image.Bounds().Max.Y // imgXRect := image.Rect(minX, minY, minX+(maxX-minX)*pixelRatio, minY+(maxY-minY)*pixelRatio) // draw.ApproxBiLinear.Scale(img, imgXRect.Add(image.Pt(int(info.Fit.X)*pixelRatio, int(info.Fit.Y)*pixelRatio)), info.image, info.image.Bounds(), draw.Src, nil) // } else { // draw.Draw(img, info.image.Bounds().Add(image.Pt(int(info.Fit.X), int(info.Fit.Y))), info.image, image.Point{}, draw.Over) // } } fileName := s.name if pixelRatio != 1 { fileName = fmt.Sprintf("%s@%dx", s.name, pixelRatio) } pngFilePath := filepath.Join(outPath, s.name, fileName+".png") var pngBuffer bytes.Buffer err := png.Encode(&pngBuffer, img) if err != nil { return err } err = os.WriteFile(pngFilePath, pngBuffer.Bytes(), 0644) if err != nil { return err } jsonFilePath := filepath.Join(outPath, s.name, fileName+".json") jsonMap := make(map[string]map[string]interface{}) for _, img := range s.infos { jsonMap[img.Name] = map[string]interface{}{ "width": img.Width * float64(pixelRatio), "height": img.Height * float64(pixelRatio), "pixelRatio": pixelRatio, "x": img.Fit.X * float64(pixelRatio), "y": img.Fit.Y * float64(pixelRatio), } } jsonBuffer, err := json.MarshalIndent(jsonMap, "", " ") if err != nil { return err } err = os.WriteFile(jsonFilePath, jsonBuffer, 0644) if err != nil { return err } return nil } func (i *ImageInfo) ReadInfo() { file, err := os.Open(i.Path) if err != nil { return } defer file.Close() if i.Type == ".png" { pngImg, err := png.Decode(file) if err != nil { return } width := pngImg.Bounds().Dx() height := pngImg.Bounds().Dy() i.Width = float64(width) i.Height = float64(height) // pngImgx := image.NewRGBA(image.Rect(0, 0, width, height)) // draw.ApproxBiLinear.Scale(pngImgx, pngImgx.Bounds(), pngImg, pngImg.Bounds(), draw.Src, nil) // i.image = pngImgx return } if i.Type == ".svg" { svgInfo, err := oksvg.ReadIconStream(file, oksvg.WarnErrorMode) if err != nil { return } // baseScale := 8.0 baseWidth := math.Ceil(svgInfo.ViewBox.W) baseHeight := math.Ceil(svgInfo.ViewBox.H) // svgImg := image.NewRGBA(image.Rect(0, 0, int(baseWidth*baseScale), int(baseHeight*baseScale))) // draw.Draw(svgImg, svgImg.Bounds(), image.Transparent, image.Point{}, draw.Src) // svgInfo.SetTarget(-svgInfo.ViewBox.X*baseScale, -svgInfo.ViewBox.Y*baseScale, svgInfo.ViewBox.W*baseScale, svgInfo.ViewBox.H*baseScale) // scaner := rasterx.NewScannerGV(int(baseWidth*baseScale), int(baseHeight*baseScale), svgImg, svgImg.Bounds()) // raster := rasterx.NewDasher(int(baseWidth*baseScale), int(baseHeight*baseScale), scaner) // svgInfo.Draw(raster, 1.0) i.Width = baseWidth i.Height = baseHeight // svgImgx := image.NewRGBA(image.Rect(0, 0, int(i.Width), int(i.Height))) // draw.ApproxBiLinear.Scale(svgImgx, svgImgx.Bounds(), svgImg, svgImg.Bounds(), draw.Src, nil) // i.image = svgImgx return } } func (i *ImageInfo) ReadImg(pixelRatio int) image.Image { file, err := os.Open(i.Path) if err != nil { return nil } defer file.Close() if i.Type == ".png" { pngImg, err := png.Decode(file) if err != nil { return nil } width := pngImg.Bounds().Dx() * pixelRatio height := pngImg.Bounds().Dy() * pixelRatio pngImgx := image.NewRGBA(image.Rect(0, 0, width, height)) draw.ApproxBiLinear.Scale(pngImgx, pngImgx.Bounds(), pngImg, pngImg.Bounds(), draw.Src, nil) return pngImgx } if i.Type == ".svg" { svgInfo, err := oksvg.ReadIconStream(file, oksvg.WarnErrorMode) if err != nil { return nil } baseScale := 8.0 baseWidth := math.Ceil(svgInfo.ViewBox.W) baseHeight := math.Ceil(svgInfo.ViewBox.H) svgImg := image.NewRGBA(image.Rect(0, 0, int(baseWidth*baseScale), int(baseHeight*baseScale))) draw.Draw(svgImg, svgImg.Bounds(), image.Transparent, image.Point{}, draw.Src) svgInfo.SetTarget(-svgInfo.ViewBox.X*baseScale, -svgInfo.ViewBox.Y*baseScale, svgInfo.ViewBox.W*baseScale, svgInfo.ViewBox.H*baseScale) scaner := rasterx.NewScannerGV(int(baseWidth*baseScale), int(baseHeight*baseScale), svgImg, svgImg.Bounds()) raster := rasterx.NewDasher(int(baseWidth*baseScale), int(baseHeight*baseScale), scaner) svgInfo.Draw(raster, 1.0) svgImgx := image.NewRGBA(image.Rect(0, 0, int(baseWidth)*pixelRatio, int(baseHeight)*pixelRatio)) draw.ApproxBiLinear.Scale(svgImgx, svgImgx.Bounds(), svgImg, svgImg.Bounds(), draw.Src, nil) return svgImgx } return nil }