├── .github └── workflows │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── README_EN.md ├── go.mod ├── input.png ├── main.go ├── output.png ├── pack.sh ├── smalloutput.png └── test └── the witcher3 ├── wp1854626-the-witcher-3-wallpapers.jpg ├── wp1854627-the-witcher-3-wallpapers.png ├── wp1854628-the-witcher-3-wallpapers.jpg ├── wp1854629-the-witcher-3-wallpapers.png ├── wp1854635-the-witcher-3-wallpapers.jpg ├── wp1854636-the-witcher-3-wallpapers.jpg ├── wp1854637-the-witcher-3-wallpapers.png ├── wp1854638-the-witcher-3-wallpapers.jpg ├── wp1854640-the-witcher-3-wallpapers.jpg ├── wp1854641-the-witcher-3-wallpapers.jpg ├── wp1854642-the-witcher-3-wallpapers.jpg ├── wp1854643-the-witcher-3-wallpapers.jpg ├── wp1854644-the-witcher-3-wallpapers.jpg ├── wp1854670-the-witcher-3-wallpapers.png ├── wp1854671-the-witcher-3-wallpapers.jpg ├── wp1854672-the-witcher-3-wallpapers.jpg ├── wp1854673-the-witcher-3-wallpapers.jpg ├── wp1854716-the-witcher-3-wallpapers.jpg ├── wp1854723-the-witcher-3-wallpapers.jpg ├── wp1854746-the-witcher-3-wallpapers.jpg ├── wp1854776-the-witcher-3-wallpapers.png ├── wp1854777-the-witcher-3-wallpapers.jpg ├── wp1854784-the-witcher-3-wallpapers.jpg ├── wp1854785-the-witcher-3-wallpapers.jpg ├── wp1854787-the-witcher-3-wallpapers.jpg ├── wp1854844-the-witcher-3-wallpapers.jpg ├── wp1854853-the-witcher-3-wallpapers.jpg ├── wp1854864-the-witcher-3-wallpapers.png ├── wp1854865-the-witcher-3-wallpapers.jpg ├── wp1854866-the-witcher-3-wallpapers.jpg ├── wp1854868-the-witcher-3-wallpapers.jpg ├── wp1854869-the-witcher-3-wallpapers.jpg ├── wp1854872-the-witcher-3-wallpapers.jpg ├── wp1854874-the-witcher-3-wallpapers.jpg ├── wp1854875-the-witcher-3-wallpapers.jpg ├── wp1854876-the-witcher-3-wallpapers.jpg ├── wp1854885-the-witcher-3-wallpapers.jpg ├── wp1854887-the-witcher-3-wallpapers.jpg ├── wp1854896-the-witcher-3-wallpapers.jpg ├── wp1854900-the-witcher-3-wallpapers.png ├── wp1854903-the-witcher-3-wallpapers.jpg ├── wp1854908-the-witcher-3-wallpapers.jpg ├── wp1854910-the-witcher-3-wallpapers.jpg ├── wp1854911-the-witcher-3-wallpapers.jpg ├── wp1854914-the-witcher-3-wallpapers.jpg ├── wp1854915-the-witcher-3-wallpapers.png ├── wp582338-the-witcher-3-wallpapers.jpg └── wp775029-the-witcher-3-wallpapers.jpg /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.19 23 | 24 | - name: Build 25 | run: | 26 | go mod tidy 27 | go build -v ./... 28 | 29 | - name: Test 30 | run: go test -v ./... 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang AS build-env 2 | 3 | RUN GO111MODULE=off go get -u github.com/esrrhs/go-mosaic 4 | RUN GO111MODULE=off go get -u github.com/esrrhs/go-mosaic/... 5 | RUN GO111MODULE=off go install github.com/esrrhs/go-mosaic 6 | 7 | FROM debian 8 | COPY --from=build-env /go/bin/go-mosaic . 9 | WORKDIR ./ 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 zhao xin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-mosaic 2 | 3 | [](https://github.com/esrrhs/go-mosaic) 4 | [](https://github.com/esrrhs/go-mosaic) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/esrrhs/go-mosaic)](https://goreportcard.com/report/github.com/esrrhs/go-mosaic) 6 | [](https://github.com/esrrhs/go-mosaic/releases) 7 | [](https://github.com/esrrhs/go-mosaic/releases) 8 | [](https://hub.docker.com/repository/docker/esrrhs/go-mosaic) 9 | [](https://github.com/esrrhs/go-mosaic/actions) 10 | 11 | go-mosaic是一个制作相片马赛克的工具。相片馬賽克,或稱蒙太奇照片、蒙太奇拼貼,是一種影像處理的藝術技巧,利用這個方式做出來的圖片,近看時是由許多張小照片合在一起的,但遠看時,每張照片透過光影和色彩的微調,組成了一張大圖的基本像素,就叫做相片馬賽克技巧 12 | 13 | [README_EN](./README_EN.md) 14 | 15 | # 特性 16 | * 专为海量图片设计,可支持数百万张图片 17 | * 内建缓存数据库,图片删除、更改自动从缓存剔除 18 | * 多核构建,加载、计算、替换均为并发 19 | 20 | # 使用 21 | * 克隆项目,编译,或者下载[release](https://github.com/esrrhs/go-mosaic/releases) 22 | * 执行命令,等待完成 23 | ``` 24 | go-mosaic.exe -src input.png -target output.jpg -lib ./test 25 | ``` 26 | * 其中./test为图片文件夹,用来组成最终图片的元素。input.png为目标图片,用来生成最终的大图output.jpg。素材图片越多,生成越精确 27 | * 更多参数,参考help 28 | ``` 29 | Usage of D:\project\go-mosaic\test.exe: 30 | -checkhash 31 | check database pic hash (default true) 32 | -database string 33 | cache datbase (default "./database.bin") 34 | -lib string 35 | image lib path 36 | -libname string 37 | image lib name in database (default "default") 38 | -maxsize int 39 | pic max size in GB (default 4) 40 | -pixelsize int 41 | pic scale size per one pixel (default 64) 42 | -scalealg string 43 | pic scale function NearestNeighbor/ApproxBiLinear/BiLinear/CatmullRom (default "CatmullRom") 44 | -src string 45 | src image path 46 | -srcsize int 47 | src image auto scale pixel size (default 128) 48 | -target string 49 | target image path 50 | -worker int 51 | worker thread num (default 12) 52 | ``` 53 | 54 | # 示例 55 | ![image](input.png) 56 | ![image](smalloutput.png) 57 | 58 | 59 | -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # go-mosaic 2 | 3 | [](https://github.com/esrrhs/go-mosaic) 4 | [](https://github.com/esrrhs/go-mosaic) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/esrrhs/go-mosaic)](https://goreportcard.com/report/github.com/esrrhs/go-mosaic) 6 | [](https://github.com/esrrhs/go-mosaic/releases) 7 | [](https://github.com/esrrhs/go-mosaic/releases) 8 | [](https://hub.docker.com/repository/docker/esrrhs/go-mosaic) 9 | [](https://github.com/esrrhs/go-mosaic/actions) 10 | 11 | go-mosaic is a tool for making photo mosaics. Photo mosaics, or montage photos, montage collages, are an art technique of image processing. Pictures made in this way are composed of many small photos when viewed close, but when viewed from a distance, each photo Through the fine-tuning of light and shadow and color, the basic pixels of a large picture are called the photo mosaic technique 12 | 13 | # Features 14 | * Designed for massive pictures, can support millions of pictures 15 | * Built-in cache database, pictures are deleted and changed automatically from cache 16 | * Multi-core construction, loading, calculation and replacement are all concurrent 17 | 18 | # Use 19 | * Clone the project, compile, or download [release](https://github.com/esrrhs/go-mosaic/releases) 20 | * Execute commands and wait for completion 21 | ``` 22 | go-mosaic.exe -src input.png -target output.jpg -lib ./test 23 | ``` 24 | * Among them, ./test is the picture folder, which is used to form the elements of the final picture. input.png is the target image, which is used to generate the final large image output.jpg. The more material pictures, the more accurate the generation 25 | * For more parameters, refer to help 26 | ``` 27 | Usage of D:\project\go-mosaic\test.exe: 28 | -checkhash 29 | check database pic hash (default true) 30 | -database string 31 | cache datbase (default "./database.bin") 32 | -lib string 33 | image lib path 34 | -libname string 35 | image lib name in database (default "default") 36 | -maxsize int 37 | pic max size in GB (default 4) 38 | -pixelsize int 39 | pic scale size per one pixel (default 64) 40 | -scalealg string 41 | pic scale function NearestNeighbor/ApproxBiLinear/BiLinear/CatmullRom (default "CatmullRom") 42 | -src string 43 | src image path 44 | -srcsize int 45 | src image auto scale pixel size (default 128) 46 | -target string 47 | target image path 48 | -worker int 49 | worker thread num (default 12) 50 | ``` 51 | 52 | # Example 53 | ![image](input.png) 54 | ![image](smalloutput.png) 55 | 56 | 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/esrrhs/go-mosaic 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/boltdb/bolt v1.3.1 7 | github.com/esrrhs/gohome v0.0.0-20230318054539-1132b09f7684 8 | golang.org/x/image v0.18.0 9 | ) 10 | 11 | require ( 12 | github.com/OneOfOne/xxhash v1.2.8 // indirect 13 | github.com/google/uuid v1.3.0 // indirect 14 | golang.org/x/sys v0.5.0 // indirect 15 | google.golang.org/protobuf v1.33.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/input.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "github.com/boltdb/bolt" 10 | "github.com/esrrhs/gohome/common" 11 | "github.com/esrrhs/gohome/loggo" 12 | "github.com/esrrhs/gohome/threadpool" 13 | "golang.org/x/image/draw" 14 | "image" 15 | "image/color" 16 | _ "image/gif" 17 | "image/jpeg" 18 | _ "image/jpeg" 19 | "image/png" 20 | _ "image/png" 21 | "io/ioutil" 22 | "math" 23 | "os" 24 | "path/filepath" 25 | "strconv" 26 | "strings" 27 | "sync" 28 | "sync/atomic" 29 | "time" 30 | ) 31 | 32 | func main() { 33 | 34 | defer common.CrashLog() 35 | 36 | src := flag.String("src", "", "src image path") 37 | target := flag.String("target", "", "target image path") 38 | lib := flag.String("lib", "", "image lib path") 39 | worker := flag.Int("worker", 12, "worker thread num") 40 | database := flag.String("database", "./database.bin", "cache datbase") 41 | pixelsize := flag.Int("pixelsize", 64, "pic scale size per one pixel") 42 | scalealg := flag.String("scalealg", "CatmullRom", "pic scale function NearestNeighbor/ApproxBiLinear/BiLinear/CatmullRom") 43 | checkhash := flag.Bool("checkhash", true, "check database pic hash") 44 | maxsize := flag.Int("maxsize", 4, "pic max size in GB") 45 | libname := flag.String("libname", "default", "image lib name in database") 46 | srcsize := flag.Int("srcsize", 128, "src image auto scale pixel size") 47 | 48 | flag.Parse() 49 | 50 | if *src == "" || *target == "" || *lib == "" { 51 | fmt.Println("need src target lib") 52 | flag.Usage() 53 | return 54 | } 55 | if getScaler(*scalealg) == nil { 56 | fmt.Println("scalealg type error") 57 | flag.Usage() 58 | return 59 | } 60 | if !strings.HasSuffix(strings.ToLower(*target), ".png") && 61 | !strings.HasSuffix(strings.ToLower(*target), ".jpg") { 62 | fmt.Println("target type error, png/jpg") 63 | flag.Usage() 64 | return 65 | } 66 | 67 | level := loggo.LEVEL_INFO 68 | loggo.Ini(loggo.Config{ 69 | Level: level, 70 | Prefix: "mosaic", 71 | MaxDay: 3, 72 | }) 73 | loggo.Info("start...") 74 | 75 | loggo.Info("src %s", *src) 76 | loggo.Info("target %s", *target) 77 | loggo.Info("lib %s", *lib) 78 | 79 | err, srcimg, cachemap := parse_src(*src, *scalealg, *srcsize) 80 | if err != nil { 81 | return 82 | } 83 | err = load_lib(*lib, *worker, *database, *pixelsize, *scalealg, *checkhash, *libname) 84 | if err != nil { 85 | return 86 | } 87 | err = gen_target(srcimg, *target, *worker, *database, *pixelsize, *maxsize, *scalealg, *libname, cachemap) 88 | if err != nil { 89 | return 90 | } 91 | } 92 | 93 | type CacheInfo struct { 94 | num int 95 | img []image.Image 96 | lock sync.Mutex 97 | } 98 | 99 | func parse_src(src string, scalealg string, srcsize int) (error, image.Image, *sync.Map) { 100 | loggo.Info("parse_src %s", src) 101 | 102 | reader, err := os.Open(src) 103 | if err != nil { 104 | loggo.Error("parse_src Open fail %s %s", src, err) 105 | return err, nil, nil 106 | } 107 | defer reader.Close() 108 | 109 | fi, err := reader.Stat() 110 | if err != nil { 111 | loggo.Error("parse_src Stat fail %s %s", src, err) 112 | return err, nil, nil 113 | } 114 | filesize := fi.Size() 115 | 116 | img, _, err := image.Decode(reader) 117 | if err != nil { 118 | loggo.Error("parse_src Decode image fail %s %s", src, err) 119 | return err, nil, nil 120 | } 121 | 122 | scale := getScaler(scalealg) 123 | 124 | lenx := img.Bounds().Dx() 125 | leny := img.Bounds().Dy() 126 | len := common.MaxOfInt(lenx, leny) 127 | if len > srcsize { 128 | newlenx := lenx * srcsize / len 129 | newleny := leny * srcsize / len 130 | rect := image.Rectangle{image.Point{0, 0}, image.Point{newlenx, newleny}} 131 | dst := image.NewRGBA(rect) 132 | scale.Scale(dst, rect, img, img.Bounds(), draw.Over, nil) 133 | img = dst 134 | } 135 | 136 | bounds := img.Bounds() 137 | 138 | startx := bounds.Min.X 139 | starty := bounds.Min.Y 140 | endx := bounds.Max.X 141 | endy := bounds.Max.Y 142 | 143 | pixelnum := make(map[string]int) 144 | for y := starty; y < endy; y++ { 145 | for x := startx; x < endx; x++ { 146 | r, g, b, _ := img.At(x, y).RGBA() 147 | r, g, b = r>>8, g>>8, b>>8 148 | 149 | pixelnum[make_string(uint8(r), uint8(g), uint8(b))]++ 150 | } 151 | } 152 | 153 | var cachemap sync.Map 154 | top := 0 155 | num := 0 156 | for { 157 | maxpixel := "" 158 | maxpixelnum := 0 159 | for k, v := range pixelnum { 160 | if v > maxpixelnum { 161 | maxpixelnum = v 162 | maxpixel = k 163 | } 164 | } 165 | if maxpixelnum >= 16 { 166 | cachemap.Store(maxpixel, &CacheInfo{num: maxpixelnum}) 167 | num++ 168 | } else { 169 | break 170 | } 171 | if maxpixelnum > top { 172 | top = maxpixelnum 173 | } 174 | pixelnum[maxpixel] = 0 175 | } 176 | 177 | loggo.Info("parse_src cache top pixel num=%d max=%d", num, top) 178 | for i := 2; i <= top; i++ { 179 | cachemap.Range(func(key, value interface{}) bool { 180 | ci := value.(*CacheInfo) 181 | if ci.num == i { 182 | loggo.Info("parse_src cache top pixel [%s]=%d", key, i) 183 | } 184 | return true 185 | }) 186 | } 187 | 188 | loggo.Info("parse_src ok %s %d %d*%d", src, filesize, img.Bounds().Dx(), img.Bounds().Dy()) 189 | return nil, img, &cachemap 190 | } 191 | 192 | func getScaler(scalealg string) draw.Scaler { 193 | var scale draw.Scaler 194 | if scalealg == "NearestNeighbor" { 195 | scale = draw.NearestNeighbor 196 | } else if scalealg == "ApproxBiLinear" { 197 | scale = draw.ApproxBiLinear 198 | } else if scalealg == "BiLinear" { 199 | scale = draw.BiLinear 200 | } else if scalealg == "CatmullRom" { 201 | scale = draw.CatmullRom 202 | } 203 | return scale 204 | } 205 | 206 | type FileInfo struct { 207 | Filename string 208 | R uint8 209 | G uint8 210 | B uint8 211 | Hash string 212 | } 213 | 214 | type CalFileInfo struct { 215 | fi FileInfo 216 | ok bool 217 | done bool 218 | } 219 | 220 | type ColorData struct { 221 | file int 222 | r uint8 223 | g uint8 224 | b uint8 225 | } 226 | 227 | func load_lib(lib string, workernum int, database string, pixelsize int, scalealg string, checkhash bool, libname string) error { 228 | loggo.Info("load_lib %s", lib) 229 | 230 | loggo.Info("load_lib start ini database") 231 | var colordata []ColorData 232 | for i := 0; i <= 255; i++ { 233 | for j := 0; j <= 255; j++ { 234 | for z := 0; z <= 255; z++ { 235 | colordata = append(colordata, ColorData{}) 236 | } 237 | } 238 | } 239 | 240 | for i := 0; i <= 255; i++ { 241 | for j := 0; j <= 255; j++ { 242 | for z := 0; z <= 255; z++ { 243 | k := make_key(uint8(i), uint8(j), uint8(z)) 244 | colordata[k].r, colordata[k].g, colordata[k].b = uint8(i), uint8(j), uint8(z) 245 | } 246 | } 247 | } 248 | 249 | loggo.Info("load_lib ini database ok") 250 | 251 | loggo.Info("load_lib start load database") 252 | 253 | db, err := bolt.Open(database, 0600, nil) 254 | if err != nil { 255 | loggo.Error("load_lib Open database fail %s %s", database, err) 256 | return err 257 | } 258 | defer db.Close() 259 | 260 | bucket_name := "FileInfo" + libname + strconv.Itoa(pixelsize) 261 | 262 | dbtotal := 0 263 | db.Update(func(tx *bolt.Tx) error { 264 | _, err := tx.CreateBucketIfNotExists([]byte(bucket_name)) 265 | if err != nil { 266 | loggo.Error("load_lib Open database CreateBucketIfNotExists fail %s %s %s", database, bucket_name, err) 267 | os.Exit(1) 268 | } 269 | b := tx.Bucket([]byte(bucket_name)) 270 | b.ForEach(func(k, v []byte) error { 271 | dbtotal++ 272 | return nil 273 | }) 274 | return nil 275 | }) 276 | 277 | lastload := time.Now() 278 | beginload := time.Now() 279 | var doneload int32 280 | var loading int32 281 | var doneloadsize int64 282 | var lock sync.Mutex 283 | db.Update(func(tx *bolt.Tx) error { 284 | b := tx.Bucket([]byte(bucket_name)) 285 | 286 | need_del := make([]string, 0) 287 | 288 | type LoadFileInfo struct { 289 | k, v []byte 290 | } 291 | 292 | tp := threadpool.NewThreadPool(workernum, 16, func(in interface{}) { 293 | defer atomic.AddInt32(&doneload, 1) 294 | defer atomic.AddInt32(&loading, -1) 295 | 296 | lf := in.(LoadFileInfo) 297 | 298 | var b bytes.Buffer 299 | b.Write(lf.v) 300 | 301 | dec := gob.NewDecoder(&b) 302 | var fi FileInfo 303 | err = dec.Decode(&fi) 304 | if err != nil { 305 | loggo.Error("load_lib Open database Decode fail %s %s %s", database, string(lf.k), err) 306 | lock.Lock() 307 | defer lock.Unlock() 308 | need_del = append(need_del, string(lf.k)) 309 | return 310 | } 311 | 312 | osfi, err := os.Stat(fi.Filename) 313 | if err != nil && os.IsNotExist(err) { 314 | loggo.Error("load_lib Open Filename IsNotExist, need delete %s %s %s", database, fi.Filename, err) 315 | lock.Lock() 316 | defer lock.Unlock() 317 | need_del = append(need_del, string(lf.k)) 318 | return 319 | } 320 | 321 | defer atomic.AddInt64(&doneloadsize, osfi.Size()) 322 | 323 | if checkhash { 324 | reader, err := os.Open(fi.Filename) 325 | if err != nil { 326 | loggo.Error("load_lib Open fail %s %s %s", database, fi.Filename, err) 327 | return 328 | } 329 | defer reader.Close() 330 | 331 | bytes, err := ioutil.ReadAll(reader) 332 | if err != nil { 333 | loggo.Error("load_lib ReadAll fail %s %s %s", database, fi.Filename, err) 334 | return 335 | } 336 | 337 | hashstr := common.GetXXHashString(string(bytes)) 338 | 339 | if hashstr != fi.Hash { 340 | loggo.Error("load_lib hash diff need delete %s %s %s %s", database, fi.Filename, hashstr, fi.Hash) 341 | lock.Lock() 342 | defer lock.Unlock() 343 | need_del = append(need_del, string(lf.k)) 344 | return 345 | } 346 | } 347 | }) 348 | 349 | b.ForEach(func(k, v []byte) error { 350 | 351 | for { 352 | ret := tp.AddJobTimeout(int(common.RandInt()), LoadFileInfo{k, v}, 10) 353 | if ret { 354 | atomic.AddInt32(&loading, 1) 355 | break 356 | } 357 | } 358 | 359 | if time.Now().Sub(lastload) >= time.Second { 360 | lastload = time.Now() 361 | speed := float64(doneload) / float64(int(time.Now().Sub(beginload))/int(time.Second)) 362 | left := "" 363 | if speed > 0 { 364 | left = time.Duration(int64(float64(dbtotal-int(doneload))/speed) * int64(time.Second)).String() 365 | } 366 | donesizem := doneloadsize / 1024 / 1024 367 | dataspeed := int(donesizem) / (int(time.Now().Sub(beginload)) / int(time.Second)) 368 | loggo.Info("load speed=%.2f/s percent=%d%% time=%s thead=%d progress=%d/%d data=%dM dataspeed=%dM/s", speed, int(doneload)*100/dbtotal, left, 369 | loading, doneload, dbtotal, donesizem, dataspeed) 370 | } 371 | 372 | return nil 373 | }) 374 | 375 | for loading != 0 { 376 | time.Sleep(time.Millisecond * 10) 377 | } 378 | 379 | tp.Stop() 380 | 381 | for _, k := range need_del { 382 | b.Delete([]byte(k)) 383 | } 384 | 385 | return nil 386 | }) 387 | 388 | loggo.Info("load_lib load database ok") 389 | 390 | loggo.Info("load_lib start get image file list") 391 | imagefilelist := make([]CalFileInfo, 0) 392 | cached := 0 393 | filepath.Walk(lib, func(path string, f os.FileInfo, err error) error { 394 | 395 | if f == nil || f.IsDir() { 396 | return nil 397 | } 398 | 399 | if !strings.HasSuffix(strings.ToLower(f.Name()), ".jpeg") && 400 | !strings.HasSuffix(strings.ToLower(f.Name()), ".jpg") && 401 | !strings.HasSuffix(strings.ToLower(f.Name()), ".png") && 402 | !strings.HasSuffix(strings.ToLower(f.Name()), ".gif") { 403 | return nil 404 | } 405 | 406 | abspath, err := filepath.Abs(path) 407 | if err != nil { 408 | loggo.Error("load_lib get Abs fail %s %s %s", database, path, err) 409 | return nil 410 | } 411 | 412 | db.View(func(tx *bolt.Tx) error { 413 | b := tx.Bucket([]byte(bucket_name)) 414 | v := b.Get([]byte(abspath)) 415 | if v == nil { 416 | imagefilelist = append(imagefilelist, CalFileInfo{fi: FileInfo{abspath, 0, 0, 0, ""}}) 417 | } else { 418 | cached++ 419 | } 420 | return nil 421 | }) 422 | 423 | return nil 424 | }) 425 | 426 | loggo.Info("load_lib get image file list ok %d cache %d", len(imagefilelist), cached) 427 | 428 | loggo.Info("load_lib start calc image avg color %d", len(imagefilelist)) 429 | var worker int32 430 | begin := time.Now() 431 | last := time.Now() 432 | var done int32 433 | var donesize int64 434 | 435 | atomic.AddInt32(&worker, 1) 436 | var save_inter int 437 | go save_to_database(&worker, &imagefilelist, db, &save_inter, bucket_name) 438 | 439 | scale := getScaler(scalealg) 440 | 441 | tp := threadpool.NewThreadPool(workernum, 16, func(in interface{}) { 442 | i := in.(int) 443 | calc_avg_color(&imagefilelist[i], &worker, &done, &donesize, scale, pixelsize) 444 | }) 445 | 446 | i := 0 447 | for worker != 0 { 448 | if i < len(imagefilelist) { 449 | ret := tp.AddJobTimeout(int(common.RandInt()), i, 10) 450 | if ret { 451 | atomic.AddInt32(&worker, 1) 452 | i++ 453 | } 454 | } else { 455 | time.Sleep(time.Millisecond * 10) 456 | } 457 | if time.Now().Sub(last) >= time.Second { 458 | last = time.Now() 459 | speed := float64(done) / float64(int(time.Now().Sub(begin))/int(time.Second)) 460 | left := "" 461 | if speed > 0 { 462 | left = time.Duration(int64(float64(len(imagefilelist)-int(done))/speed) * int64(time.Second)).String() 463 | } 464 | donesizem := donesize / 1024 / 1024 465 | dataspeed := int(donesizem) / (int(time.Now().Sub(begin)) / int(time.Second)) 466 | loggo.Info("calc speed=%.2f/s percent=%d%% time=%s thead=%d progress=%d/%d saved=%d data=%dM dataspeed=%dM/s", speed, int(done)*100/len(imagefilelist), 467 | left, int(worker), int(done), len(imagefilelist), save_inter, donesizem, dataspeed) 468 | } 469 | } 470 | tp.Stop() 471 | 472 | loggo.Info("load_lib calc image avg color ok %d %d", len(imagefilelist), done) 473 | 474 | loggo.Info("load_lib start save image avg color") 475 | 476 | maxcolornum := 0 477 | totalnum := 0 478 | db.View(func(tx *bolt.Tx) error { 479 | b := tx.Bucket([]byte(bucket_name)) 480 | 481 | b.ForEach(func(k, v []byte) error { 482 | 483 | var b bytes.Buffer 484 | b.Write(v) 485 | 486 | dec := gob.NewDecoder(&b) 487 | var fi FileInfo 488 | err = dec.Decode(&fi) 489 | if err != nil { 490 | loggo.Error("load_lib Open database Decode fail %s %s %s", database, string(k), err) 491 | return nil 492 | } 493 | 494 | key := make_key(fi.R, fi.G, fi.B) 495 | colordata[key].file++ 496 | if colordata[key].file > maxcolornum { 497 | maxcolornum = colordata[key].file 498 | } 499 | totalnum++ 500 | 501 | return nil 502 | }) 503 | 504 | return nil 505 | }) 506 | 507 | loggo.Info("load_lib save image avg color ok total %d max %d", totalnum, maxcolornum) 508 | 509 | if totalnum <= 0 { 510 | loggo.Error("load_lib no pic in lib %s", database) 511 | return errors.New("no pic") 512 | } 513 | 514 | tmpcolornum := make(map[int]int) 515 | tmpcolorone := make(map[int]ColorData) 516 | colorgourp := []struct { 517 | name string 518 | c color.RGBA 519 | num int 520 | }{ 521 | {"Black", common.Black, 0}, 522 | {"White", common.White, 0}, 523 | {"Red", common.Red, 0}, 524 | {"Lime", common.Lime, 0}, 525 | {"Blue", common.Blue, 0}, 526 | {"Yellow", common.Yellow, 0}, 527 | {"Cyan", common.Cyan, 0}, 528 | {"Magenta", common.Magenta, 0}, 529 | {"Silver", common.Silver, 0}, 530 | {"Gray", common.Gray, 0}, 531 | {"Maroon", common.Maroon, 0}, 532 | {"Olive", common.Olive, 0}, 533 | {"Green", common.Green, 0}, 534 | {"Purple", common.Purple, 0}, 535 | {"Teal", common.Teal, 0}, 536 | {"Navy", common.Navy, 0}, 537 | } 538 | 539 | for _, data := range colordata { 540 | tmpcolornum[data.file]++ 541 | tmpcolorone[data.file] = data 542 | 543 | if data.file > 0 { 544 | min := 0 545 | mindistance := math.MaxFloat64 546 | for index, cg := range colorgourp { 547 | diff := common.ColorDistance(color.RGBA{data.r, data.g, data.b, 0}, cg.c) 548 | if diff < mindistance { 549 | min = index 550 | mindistance = diff 551 | } 552 | } 553 | 554 | colorgourp[min].num += data.file 555 | } 556 | } 557 | 558 | for i := 0; i <= maxcolornum; i++ { 559 | str := "" 560 | if tmpcolornum[i] == 1 { 561 | str = make_string(tmpcolorone[i].r, tmpcolorone[i].g, tmpcolorone[i].b) 562 | } 563 | loggo.Info("load_lib avg color num distribution %d = %d %s", i, tmpcolornum[i], str) 564 | } 565 | 566 | maxcolorgroupnum := 0 567 | maxcolorgroupindex := 0 568 | for index, cg := range colorgourp { 569 | loggo.Info("load_lib avg color color distribution %s = %d", cg.name, cg.num) 570 | if cg.num > maxcolorgroupnum { 571 | maxcolorgroupnum = cg.num 572 | maxcolorgroupindex = index 573 | } 574 | } 575 | loggo.Info("load_lib avg color color max %s %d", colorgourp[maxcolorgroupindex].name, colorgourp[maxcolorgroupindex].num) 576 | 577 | return nil 578 | } 579 | 580 | func make_key(r uint8, g uint8, b uint8) int { 581 | return int(r)*256*256 + int(g)*256 + int(b) 582 | } 583 | 584 | func make_string(r uint8, g uint8, b uint8) string { 585 | return "r " + strconv.Itoa(int(r)) + " g " + strconv.Itoa(int(g)) + " b " + strconv.Itoa(int(b)) 586 | } 587 | 588 | func calc_img(src image.Image, filename string, scaler draw.Scaler, pixelsize int) (image.Image, error) { 589 | 590 | bounds := src.Bounds() 591 | 592 | len := common.MinOfInt(bounds.Dx(), bounds.Dy()) 593 | startx := bounds.Min.X + (bounds.Dx()-len)/2 594 | starty := bounds.Min.Y + (bounds.Dy()-len)/2 595 | endx := common.MinOfInt(startx+len, bounds.Max.X) 596 | endy := common.MinOfInt(starty+len, bounds.Max.Y) 597 | 598 | if startx != bounds.Min.X || starty != bounds.Min.Y || endx != bounds.Max.X || endy != bounds.Max.Y { 599 | dst := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{len, len}}) 600 | draw.Copy(dst, image.Point{0, 0}, src, image.Rectangle{image.Point{startx, starty}, image.Point{endx, endy}}, draw.Over, nil) 601 | src = dst 602 | } 603 | 604 | bounds = src.Bounds() 605 | if bounds.Dx() != bounds.Dy() { 606 | loggo.Error("calc_img cult image fail %s %d %d", filename, bounds.Dx(), bounds.Dy()) 607 | return nil, errors.New("bounds error") 608 | } 609 | 610 | len = common.MinOfInt(bounds.Dx(), bounds.Dy()) 611 | if len < pixelsize { 612 | loggo.Error("calc_img image too small %s %d %d", filename, len, pixelsize) 613 | return nil, errors.New("too small") 614 | } 615 | 616 | if len > pixelsize { 617 | rect := image.Rectangle{image.Point{0, 0}, image.Point{pixelsize, pixelsize}} 618 | dst := image.NewRGBA(rect) 619 | scaler.Scale(dst, rect, src, src.Bounds(), draw.Over, nil) 620 | src = dst 621 | } 622 | 623 | return src, nil 624 | } 625 | 626 | func calc_avg_color(cfi *CalFileInfo, worker *int32, done *int32, donesize *int64, scaler draw.Scaler, pixelsize int) { 627 | defer common.CrashLog() 628 | defer atomic.AddInt32(worker, -1) 629 | defer atomic.AddInt32(done, 1) 630 | defer func() { 631 | cfi.done = true 632 | }() 633 | 634 | reader, err := os.Open(cfi.fi.Filename) 635 | if err != nil { 636 | loggo.Error("calc_avg_color Open fail %s %s", cfi.fi.Filename, err) 637 | return 638 | } 639 | defer reader.Close() 640 | 641 | fi, err := reader.Stat() 642 | if err != nil { 643 | loggo.Error("calc_avg_color Stat fail %s %s", cfi.fi.Filename, err) 644 | return 645 | } 646 | filesize := fi.Size() 647 | defer atomic.AddInt64(donesize, filesize) 648 | 649 | img, _, err := image.Decode(reader) 650 | if err != nil { 651 | loggo.Error("calc_avg_color Decode image fail %s %s", cfi.fi.Filename, err) 652 | return 653 | } 654 | 655 | img, err = calc_img(img, cfi.fi.Filename, scaler, pixelsize) 656 | if err != nil { 657 | loggo.Error("calc_avg_color calc_img image fail %s %s", cfi.fi.Filename, err) 658 | return 659 | } 660 | 661 | bounds := img.Bounds() 662 | 663 | var sumR, sumG, sumB, count float64 664 | 665 | for y := bounds.Min.Y; y < bounds.Max.Y; y++ { 666 | for x := bounds.Min.X; x < bounds.Max.X; x++ { 667 | r, g, b, _ := img.At(x, y).RGBA() 668 | r, g, b = r>>8, g>>8, b>>8 669 | 670 | sumR += float64(r) 671 | sumG += float64(g) 672 | sumB += float64(b) 673 | 674 | count += 1 675 | } 676 | } 677 | 678 | readerhash, err := os.Open(cfi.fi.Filename) 679 | if err != nil { 680 | loggo.Error("calc_avg_color Open fail %s %s", cfi.fi.Filename, err) 681 | return 682 | } 683 | defer readerhash.Close() 684 | 685 | b, err := ioutil.ReadAll(readerhash) 686 | if err != nil { 687 | loggo.Error("calc_avg_color ReadAll fail %s %d", cfi.fi.Filename, pixelsize) 688 | return 689 | } 690 | 691 | cfi.fi.R = uint8(sumR / count) 692 | cfi.fi.G = uint8(sumG / count) 693 | cfi.fi.B = uint8(sumB / count) 694 | cfi.fi.Hash = common.GetXXHashString(string(b)) 695 | cfi.ok = true 696 | 697 | return 698 | } 699 | 700 | func save_to_database(worker *int32, imagefilelist *[]CalFileInfo, db *bolt.DB, save_inter *int, bucket_name string) { 701 | defer common.CrashLog() 702 | defer atomic.AddInt32(worker, -1) 703 | 704 | i := 0 705 | for { 706 | if i >= len(*imagefilelist) { 707 | return 708 | } 709 | 710 | cfi := (*imagefilelist)[i] 711 | if cfi.done { 712 | i++ 713 | 714 | if cfi.ok { 715 | var b bytes.Buffer 716 | 717 | enc := gob.NewEncoder(&b) 718 | err := enc.Encode(&cfi.fi) 719 | if err != nil { 720 | loggo.Error("calc_avg_color Encode FileInfo fail %s %s", cfi.fi.Filename, err) 721 | return 722 | } 723 | 724 | k := []byte(cfi.fi.Filename) 725 | v := b.Bytes() 726 | 727 | db.Update(func(tx *bolt.Tx) error { 728 | b := tx.Bucket([]byte(bucket_name)) 729 | err := b.Put(k, v) 730 | return err 731 | }) 732 | } 733 | 734 | *save_inter = i 735 | } else { 736 | time.Sleep(time.Millisecond * 10) 737 | } 738 | } 739 | } 740 | 741 | func gen_target(srcimg image.Image, target string, workernum int, database string, pixelsize int, maxsize int, scalealg string, libname string, cachemap *sync.Map) error { 742 | loggo.Info("gen_target %s", target) 743 | 744 | db, err := bolt.Open(database, 0600, nil) 745 | if err != nil { 746 | loggo.Error("gen_target Open database fail %s %s", database, err) 747 | return err 748 | } 749 | defer db.Close() 750 | 751 | bucket_name := "FileInfo" + libname + strconv.Itoa(pixelsize) 752 | 753 | bounds := srcimg.Bounds() 754 | 755 | startx := bounds.Min.X 756 | starty := bounds.Min.Y 757 | endx := bounds.Max.X 758 | endy := bounds.Max.Y 759 | 760 | last := time.Now() 761 | begin := time.Now() 762 | total := bounds.Dx() * bounds.Dy() 763 | var done int32 764 | var doing int32 765 | var cached int32 766 | 767 | lenx := bounds.Dx() * pixelsize 768 | leny := bounds.Dy() * pixelsize 769 | 770 | outputfilesize := lenx * leny * 4 / 1024 / 1024 / 1024 771 | if outputfilesize > maxsize { 772 | loggo.Error("gen_target too big %s %dG than %dG", target, outputfilesize, maxsize) 773 | return errors.New("too big") 774 | } 775 | 776 | loggo.Info("gen_target start gen pixel %s %dG max %dG", target, outputfilesize, maxsize) 777 | 778 | dst := image.NewRGBA(image.Rectangle{image.Point{0, 0}, image.Point{lenx, leny}}) 779 | 780 | type GenInfo struct { 781 | x int 782 | y int 783 | c color.RGBA 784 | } 785 | 786 | tp := threadpool.NewThreadPool(workernum, 16, func(in interface{}) { 787 | defer atomic.AddInt32(&done, 1) 788 | defer atomic.AddInt32(&doing, -1) 789 | gi := in.(GenInfo) 790 | gen_target_pixel(gi.c, gi.x, gi.y, dst, db, bucket_name, pixelsize, scalealg, cachemap, &cached) 791 | }) 792 | 793 | for y := starty; y < endy; y++ { 794 | for x := startx; x < endx; x++ { 795 | r, g, b, _ := srcimg.At(x, y).RGBA() 796 | r, g, b = r>>8, g>>8, b>>8 797 | 798 | for { 799 | ret := tp.AddJobTimeout(int(common.RandInt()), GenInfo{x: x, y: y, c: color.RGBA{uint8(r), uint8(g), uint8(b), 0}}, 10) 800 | if ret { 801 | atomic.AddInt32(&doing, 1) 802 | break 803 | } 804 | } 805 | 806 | if time.Now().Sub(last) >= time.Second { 807 | last = time.Now() 808 | speed := float64(done) / float64(int(time.Now().Sub(begin))/int(time.Second)) 809 | left := "" 810 | if speed > 0 { 811 | left = time.Duration(int64(float64(total-int(done))/speed) * int64(time.Second)).String() 812 | } 813 | loggo.Info("gen speed=%.2f/s percent=%d%% time=%s thead=%d progress=%d/%d cached=%d cached-percent=%d%%", speed, int(done)*100/total, 814 | left, int(doing), int(done), total, cached, int(cached)*100/total) 815 | } 816 | } 817 | } 818 | 819 | for doing != 0 { 820 | time.Sleep(time.Millisecond * 10) 821 | } 822 | 823 | tp.Stop() 824 | 825 | loggo.Info("gen_target gen pixel ok %s", target) 826 | 827 | loggo.Info("gen_target start write file %s", target) 828 | dstFile, err := os.Create(target) 829 | if err != nil { 830 | loggo.Error("gen_target Create fail %s %s", target, err) 831 | return err 832 | } 833 | defer dstFile.Close() 834 | 835 | if strings.HasSuffix(strings.ToLower(target), ".png") { 836 | err = png.Encode(dstFile, dst) 837 | } else if strings.HasSuffix(strings.ToLower(target), ".jpg") { 838 | err = jpeg.Encode(dstFile, dst, &jpeg.Options{Quality: 100}) 839 | } 840 | if err != nil { 841 | loggo.Error("gen_target Encode fail %s %s", target, err) 842 | return err 843 | } 844 | 845 | loggo.Info("gen_target write file ok %s", target) 846 | 847 | return nil 848 | } 849 | 850 | func gen_target_pixel(src color.RGBA, x int, y int, dst *image.RGBA, db *bolt.DB, bucket_name string, pixelsize int, scalealg string, cachemap *sync.Map, cached *int32) { 851 | 852 | var minimgs []image.Image 853 | 854 | key := make_string(src.R, src.G, src.B) 855 | v, ok := cachemap.Load(key) 856 | if ok { 857 | ci := v.(*CacheInfo) 858 | minimgs = ci.img 859 | } 860 | 861 | if len(minimgs) <= 0 { 862 | if ok { 863 | ci := v.(*CacheInfo) 864 | ci.lock.Lock() 865 | } 866 | 867 | if len(minimgs) <= 0 { 868 | 869 | mindiff := math.MaxFloat64 870 | var mindiffnames []string 871 | var minfi FileInfo 872 | 873 | db.View(func(tx *bolt.Tx) error { 874 | b := tx.Bucket([]byte(bucket_name)) 875 | b.ForEach(func(k, v []byte) error { 876 | 877 | var b bytes.Buffer 878 | b.Write(v) 879 | 880 | dec := gob.NewDecoder(&b) 881 | var fi FileInfo 882 | err := dec.Decode(&fi) 883 | if err != nil { 884 | loggo.Error("gen_target_pixel database Decode fail %s %s", string(k), err) 885 | os.Exit(1) 886 | } 887 | 888 | if minfi.R == fi.R && minfi.G == fi.G && minfi.B == fi.B { 889 | mindiffnames = append(mindiffnames, fi.Filename) 890 | return nil 891 | } 892 | 893 | tmp := color.RGBA{fi.R, fi.G, fi.B, 0} 894 | diff := common.ColorDistance(src, tmp) 895 | if diff < mindiff { 896 | mindiff = diff 897 | mindiffnames = mindiffnames[:0] 898 | mindiffnames = append(mindiffnames, fi.Filename) 899 | minfi = fi 900 | } 901 | 902 | return nil 903 | }) 904 | return nil 905 | }) 906 | 907 | for _, mindiffname := range mindiffnames { 908 | reader, err := os.Open(mindiffname) 909 | if err != nil { 910 | loggo.Error("gen_target_pixel Open fail %s %s", mindiffname, err) 911 | os.Exit(1) 912 | } 913 | defer reader.Close() 914 | 915 | minimg, _, err := image.Decode(reader) 916 | if err != nil { 917 | loggo.Error("gen_target_pixel Decode fail %s %s", mindiffname, err) 918 | return 919 | } 920 | 921 | scale := getScaler(scalealg) 922 | 923 | minimg, err = calc_img(minimg, mindiffname, scale, pixelsize) 924 | if err != nil { 925 | loggo.Error("gen_target_pixel calc_img image fail %s %s", mindiffname, err) 926 | return 927 | } 928 | 929 | minimgs = append(minimgs, minimg) 930 | } 931 | 932 | v, ok := cachemap.Load(key) 933 | if ok { 934 | ci := v.(*CacheInfo) 935 | ci.img = minimgs 936 | } 937 | } else { 938 | atomic.AddInt32(cached, 1) 939 | } 940 | 941 | if ok { 942 | ci := v.(*CacheInfo) 943 | ci.lock.Unlock() 944 | } 945 | } else { 946 | atomic.AddInt32(cached, 1) 947 | } 948 | 949 | var minimg image.Image 950 | minimg = minimgs[common.RandInt31n(len(minimgs))] 951 | 952 | if common.RandInt()%2 == 0 { 953 | flippedImg := image.NewRGBA(minimg.Bounds()) 954 | for j := 0; j < minimg.Bounds().Dy(); j++ { 955 | for i := 0; i < minimg.Bounds().Dx(); i++ { 956 | flippedImg.Set((minimg.Bounds().Dx()-1)-i, j, minimg.At(i, j)) 957 | } 958 | } 959 | minimg = flippedImg 960 | } 961 | 962 | draw.Copy(dst, image.Point{x * pixelsize, y * pixelsize}, minimg, minimg.Bounds(), draw.Over, nil) 963 | } 964 | -------------------------------------------------------------------------------- /output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/output.png -------------------------------------------------------------------------------- /pack.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | #set -x 3 | NAME="go-mosaic" 4 | 5 | export GO111MODULE=on 6 | 7 | #go tool dist list 8 | build_list=$(go tool dist list) 9 | 10 | rm pack -rf 11 | rm pack.zip -f 12 | mkdir pack 13 | 14 | for line in $build_list; do 15 | os=$(echo "$line" | awk -F"/" '{print $1}') 16 | arch=$(echo "$line" | awk -F"/" '{print $2}') 17 | echo "os="$os" arch="$arch" start build" 18 | if [ $os == "android" ] || [ $os == "ios" ] || [ $os == "plan9" ] || [ $arch == "ppc64" ] || [ $arch == "wasm" ] || [ $arch == "mips" ] || 19 | [ $arch == "mips64" ] || [ $arch == "mips64le" ] || [ $arch == "mipsle" ] || [ $arch == "riscv64" ]; then 20 | continue 21 | fi 22 | CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -ldflags="-s -w" 23 | if [ $? -ne 0 ]; then 24 | echo "os="$os" arch="$arch" build fail" 25 | exit 1 26 | fi 27 | if [ $os = "windows" ]; then 28 | zip ${NAME}_"${os}"_"${arch}"".zip" $NAME".exe" 29 | if [ $? -ne 0 ]; then 30 | echo "os="$os" arch="$arch" zip fail" 31 | exit 1 32 | fi 33 | mv ${NAME}_"${os}"_"${arch}"".zip" pack/ 34 | rm $NAME".exe" -f 35 | else 36 | zip ${NAME}_"${os}"_"${arch}"".zip" $NAME 37 | if [ $? -ne 0 ]; then 38 | echo "os="$os" arch="$arch" zip fail" 39 | exit 1 40 | fi 41 | mv ${NAME}_"${os}"_"${arch}"".zip" pack/ 42 | rm $NAME -f 43 | fi 44 | echo "os="$os" arch="$arch" done build" 45 | done 46 | 47 | zip pack.zip pack/ -r 48 | 49 | echo "all done" 50 | -------------------------------------------------------------------------------- /smalloutput.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/smalloutput.png -------------------------------------------------------------------------------- /test/the witcher3/wp1854626-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854626-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854627-the-witcher-3-wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854627-the-witcher-3-wallpapers.png -------------------------------------------------------------------------------- /test/the witcher3/wp1854628-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854628-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854629-the-witcher-3-wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854629-the-witcher-3-wallpapers.png -------------------------------------------------------------------------------- /test/the witcher3/wp1854635-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854635-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854636-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854636-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854637-the-witcher-3-wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854637-the-witcher-3-wallpapers.png -------------------------------------------------------------------------------- /test/the witcher3/wp1854638-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854638-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854640-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854640-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854641-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854641-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854642-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854642-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854643-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854643-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854644-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854644-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854670-the-witcher-3-wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854670-the-witcher-3-wallpapers.png -------------------------------------------------------------------------------- /test/the witcher3/wp1854671-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854671-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854672-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854672-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854673-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854673-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854716-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854716-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854723-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854723-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854746-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854746-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854776-the-witcher-3-wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854776-the-witcher-3-wallpapers.png -------------------------------------------------------------------------------- /test/the witcher3/wp1854777-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854777-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854784-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854784-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854785-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854785-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854787-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854787-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854844-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854844-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854853-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854853-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854864-the-witcher-3-wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854864-the-witcher-3-wallpapers.png -------------------------------------------------------------------------------- /test/the witcher3/wp1854865-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854865-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854866-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854866-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854868-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854868-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854869-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854869-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854872-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854872-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854874-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854874-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854875-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854875-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854876-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854876-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854885-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854885-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854887-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854887-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854896-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854896-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854900-the-witcher-3-wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854900-the-witcher-3-wallpapers.png -------------------------------------------------------------------------------- /test/the witcher3/wp1854903-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854903-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854908-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854908-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854910-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854910-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854911-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854911-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854914-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854914-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp1854915-the-witcher-3-wallpapers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp1854915-the-witcher-3-wallpapers.png -------------------------------------------------------------------------------- /test/the witcher3/wp582338-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp582338-the-witcher-3-wallpapers.jpg -------------------------------------------------------------------------------- /test/the witcher3/wp775029-the-witcher-3-wallpapers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/esrrhs/go-mosaic/63eb781456541e70cf9553a606e811e7aba9e91c/test/the witcher3/wp775029-the-witcher-3-wallpapers.jpg --------------------------------------------------------------------------------