// Copyright 2014 by Sascha L. Teichmann // Use of this source code is governed by the MIT license // that can be found in the LICENSE file. package main import ( "bufio" "fmt" "image" "image/draw" "image/png" "io/ioutil" "log" "math" "os" "path/filepath" "strconv" "strings" "sync" "github.com/nfnt/resize" ) func findMaxDir(files []os.FileInfo) (min, max int) { min, max = math.MaxInt32, math.MinInt32 for _, file := range files { if !file.Mode().IsDir() { continue } if x, err := strconv.Atoi(file.Name()); err == nil { if x > max { max = x } if x < min { min = x } } } return } func findMaxFile(files []os.FileInfo) (min, max int) { min, max = math.MaxInt32, math.MinInt32 for _, file := range files { if !file.Mode().IsRegular() { continue } name := file.Name() name = strings.TrimSuffix(name, filepath.Ext(name)) if x, err := strconv.Atoi(name); err == nil { if x > max { max = x } if x < min { min = x } } } return } type pyramidJob struct { src [4]string dst string } func createParentLevel(oldDir string, jobs chan pyramidJob) (newDir string, err error) { oldName := filepath.Base(oldDir) var oldLevel int if oldLevel, err = strconv.Atoi(oldName); err != nil { return } if oldLevel <= 0 { return } var files []os.FileInfo if files, err = ioutil.ReadDir(oldDir); err != nil { return } xMin, xMax := findMaxDir(files) if xMax == math.MinInt32 { return } newLevel := oldLevel - 1 log.Printf("Generating tiles of level %d", newLevel) parentDir := filepath.Dir(oldDir) newDir = filepath.Join(parentDir, strconv.Itoa(newLevel)) if err = os.MkdirAll(newDir, os.ModePerm); err != nil { return } for ox, nx := xMin, xMin; ox <= xMax; ox += 2 { ox1Dir := filepath.Join(oldDir, strconv.Itoa(ox)) ox2Dir := filepath.Join(oldDir, strconv.Itoa(ox+1)) if files, err = ioutil.ReadDir(ox1Dir); err != nil { return } zMin, zMax := findMaxFile(files) if zMax == math.MinInt32 { nx++ continue } nxDir := filepath.Join(newDir, strconv.Itoa(nx)) if err = os.MkdirAll(nxDir, os.ModePerm); err != nil { return } for oz, nz := zMin, zMin; oz <= zMax; oz += 2 { oz1 := fmt.Sprintf("%d.png", oz) oz2 := fmt.Sprintf("%d.png", oz+1) s1 := filepath.Join(ox1Dir, oz1) s2 := filepath.Join(ox1Dir, oz2) s3 := filepath.Join(ox2Dir, oz1) s4 := filepath.Join(ox2Dir, oz2) d := filepath.Join(nxDir, fmt.Sprintf("%d.png", nz)) jobs <- pyramidJob{src: [4]string{s1, s2, s3, s4}, dst: d} nz++ } nx++ } return } func loadImage(path string) image.Image { var err error var file *os.File if file, err = os.Open(path); err != nil { //log.Printf("WARN: %s", err) return image.White } defer file.Close() reader := bufio.NewReader(file) var img image.Image if img, err = png.Decode(reader); err != nil { log.Printf("WARN: %s", err) return image.White } return img } func fuseTile(scratch *image.RGBA, job *pyramidJob) (err error) { var ofs = [4][2]int{ {0, 0}, {0, 256}, {256, 0}, {256, 256}} for i, path := range job.src { img := loadImage(path) draw.Draw(scratch, image.Rect(ofs[i][0], ofs[i][1], 256, 256), img, image.ZP, draw.Src) } resized := resize.Resize(256, 256, scratch, resize.Bicubic) var outFile *os.File if outFile, err = os.Create(job.dst); err != nil { return } out := bufio.NewWriter(outFile) if err = png.Encode(out, resized); err != nil { outFile.Close() return } out.Flush() err = outFile.Close() return } func fuseTiles(jobs chan pyramidJob, done *sync.WaitGroup) { defer done.Done() scratch := image.NewRGBA(image.Rect(0, 0, 512, 512)) for job := range jobs { if err := fuseTile(scratch, &job); err != nil { log.Printf("WARN: Writing image failed: %s", err) } } } func createPyramid(outDir string, numWorker int) (err error) { for oldDir := filepath.Join(outDir, baseLevelDir); oldDir != ""; { if oldDir, err = createLevel(oldDir, numWorker); err != nil { return } } return } func createLevel(oldDir string, numWorker int) (newDir string, err error) { jobs := make(chan pyramidJob) var done sync.WaitGroup for i := 0; i < numWorker; i++ { done.Add(1) go fuseTiles(jobs, &done) } newDir, err = createParentLevel(oldDir, jobs) close(jobs) if err != nil { return } done.Wait() return }