diff --git a/go.mod b/go.mod index 0de7c15..438aca3 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,13 @@ go 1.22.1 require github.com/disintegration/imaging v1.6.2 -require golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) + +require ( + github.com/stretchr/testify v1.9.0 + golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 // indirect +) diff --git a/go.sum b/go.sum index 6e27fcd..507dfe1 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index 638d277..eed22b7 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "image/png" "io" "log" + "math" "os" "path/filepath" "sync" @@ -41,6 +42,8 @@ func (rp *resultPrinter) Run() { } } +const defaultPathTemplate = "%s-%d-%d-%d.%s" + func main() { t0 := time.Now() filenamePtr := flag.String("filename", "", "filename to open") @@ -48,7 +51,7 @@ func main() { concurrencyPtr := flag.Int("concurrency", 5, "how many tiles to generate concurrently (threads)") baseName := flag.String("basename", "tile", "base of the output files") outFormat := flag.String("format", "png", "output format (jpg/png)") - pathTemplate := flag.String("path-template", "%s-%d-%d-%d.%s", "template for output files - base, zoom, x, y, format") + pathTemplate := flag.String("path-template", defaultPathTemplate, "template for output files - base, zoom, x, y, format") flag.Parse() @@ -63,16 +66,27 @@ func main() { } log.Println("opening file:", *filenamePtr) - src, err := imaging.Open(*filenamePtr) + src, err := os.Open(*filenamePtr) if err != nil { fmt.Println("Error: Could not open file:", err) return } + processImage(src, *baseName, *pathTemplate, *outFormat, *tileSizePtr, *concurrencyPtr, diskOutput{}) + log.Printf("done in %.2f", time.Since(t0).Seconds()) +} + +func processImage(input io.Reader, basename string, pathTemplate string, format string, tileSize int, concurrency int, output outputter) { + + src, err := imaging.Decode(input) + if err != nil { + panic(err) + } + size := src.Bounds().Max - tile_size_x := *tileSizePtr - tile_size_y := *tileSizePtr + tile_size_x := tileSize + tile_size_y := tileSize // work out maximum zoom var max_zoom int @@ -88,9 +102,6 @@ func main() { z := max_zoom log.Println("maximum zoom level is", max_zoom) - - concurrency := *concurrencyPtr - log.Println("starting tiling with concurrency of", concurrency) results := make(chan string) @@ -126,8 +137,16 @@ func main() { log.Printf("zoom level: %d (%d x %d)\n", z, size.X, size.Y) - yTiles := (size.Y / tile_size_y) - xTiles := (size.X / tile_size_x) + // size 257 => 2 + // size 256 => 1 + // size 255 => 1 + yTiles := int(math.Ceil(float64(size.Y) / float64(tile_size_y))) + xTiles := int(math.Ceil(float64(size.X) / float64(tile_size_x))) + + if z == 0 { + xTiles = 1 + yTiles = 1 + } tilesToRender := xTiles * yTiles rp.Reset(tilesToRender) @@ -135,12 +154,14 @@ func main() { wg := sync.WaitGroup{} wg.Add(tilesToRender) + log.Printf("rendering %d tiles", tilesToRender) + for y := 0; y < yTiles; y++ { for x := 0; x < xTiles; x++ { jobs <- tileJob{ - baseName: *baseName, - pathTemplate: *pathTemplate, - format: *outFormat, + baseName: basename, + pathTemplate: pathTemplate, + format: format, src: src, zoom: z, x: x, @@ -148,6 +169,7 @@ func main() { tileSizeX: tile_size_x, tileSizeY: tile_size_y, wg: &wg, + output: output, } } @@ -164,11 +186,17 @@ func main() { fmt.Print("\033[u\033[K") // restore the cursor position and clear the line } close(results) - log.Printf("done in %.2f", time.Since(t0).Seconds()) - + log.Printf("all tiles complete") } -func createPathAndFile(fn string) (io.WriteCloser, error) { +type outputter interface { + CreatePathAndFile(fn string) (io.WriteCloser, error) +} + +type diskOutput struct { +} + +func (do diskOutput) CreatePathAndFile(fn string) (io.WriteCloser, error) { dir, _ := filepath.Split(fn) err := os.MkdirAll(dir, 0777) if err != nil { @@ -188,6 +216,7 @@ type tileJob struct { x, y int tileSizeX int tileSizeY int + output outputter wg *sync.WaitGroup } @@ -197,7 +226,7 @@ func tileWorker(jobs <-chan tileJob, results chan<- string) { cropped := imaging.Crop(j.src, image.Rect(j.tileSizeX*j.x, j.tileSizeY*j.y, j.tileSizeX*j.x+j.tileSizeX, j.tileSizeY*j.y+j.tileSizeY)) // log.Printf("writing to %s", output_filename) - writer, err := createPathAndFile(output_filename) + writer, err := j.output.CreatePathAndFile(output_filename) if err != nil { panic(err) } @@ -207,6 +236,8 @@ func tileWorker(jobs <-chan tileJob, results chan<- string) { err = jpeg.Encode(writer, cropped, &jpeg.Options{ Quality: 40, }) + } else { + panic("bad format") } if err != nil { panic(err) diff --git a/slice_test.go b/slice_test.go new file mode 100644 index 0000000..0cc5e7b --- /dev/null +++ b/slice_test.go @@ -0,0 +1,92 @@ +package main + +import ( + "fmt" + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +type testWriteCloser struct { + written int + closed bool +} + +func (twc *testWriteCloser) Close() error { + twc.closed = true + return nil +} + +func (twc *testWriteCloser) Write(b []byte) (int, error) { + twc.written += len(b) + return len(b), nil +} + +type testOutputter struct { + closers map[string]*testWriteCloser +} + +func (to *testOutputter) CreatePathAndFile(fn string) (io.WriteCloser, error) { + to.closers[fn] = &testWriteCloser{} + return to.closers[fn], nil +} + +func (to *testOutputter) Dump() string { + out := "" + for k, v := range to.closers { + out += fmt.Sprintf("%20s: %6d/%t\n", k, v.written, v.closed) + } + return out +} + +func NewTestOutputter() *testOutputter { + to := testOutputter{} + to.closers = make(map[string]*testWriteCloser) + return &to +} + +func TestXxx(t *testing.T) { + + // testdata/gold.jpg: JPEG image data, JFIF standard 1.01, 794x447 + f, err := os.Open("testdata/gold.jpg") + if err != nil { + panic(err) + } + + // with a tile size larger than the image itself, we should + // get a single image at zoom level 0 + testOutputter := NewTestOutputter() + processImage(f, "indy", defaultPathTemplate, "jpg", 1000, 1, testOutputter) + t.Log(testOutputter.Dump()) + assert.Equal(t, 1, len(testOutputter.closers)) + assert.Equal(t, 28082, testOutputter.closers["indy-0-0-0.jpg"].written) + + f, _ = os.Open("testdata/gold.jpg") + + // with a tilesize smaller than *one* of the dimensions, 3 tiles + // 1@zoom 0, 2@zoom 1 + testOutputter = NewTestOutputter() + processImage(f, "indy", defaultPathTemplate, "jpg", 500, 1, testOutputter) + t.Log(testOutputter.Dump()) + assert.Equal(t, 3, len(testOutputter.closers)) + assert.Equal(t, 10304, testOutputter.closers["indy-0-0-0.jpg"].written) + assert.Equal(t, 18014, testOutputter.closers["indy-1-0-0.jpg"].written) + assert.Equal(t, 10749, testOutputter.closers["indy-1-1-0.jpg"].written) + + f, _ = os.Open("testdata/gold.jpg") + + // with a tilesize smaller than *both* of the dimensions, 5 tiles + // zoom 0, zoom 1 x 4 + testOutputter = NewTestOutputter() + processImage(f, "indy", defaultPathTemplate, "jpg", 400, 1, testOutputter) + t.Log(testOutputter.Dump()) + assert.Equal(t, 5, len(testOutputter.closers)) + assert.Equal(t, 10304, testOutputter.closers["indy-0-0-0.jpg"].written) + assert.Equal(t, 12607, testOutputter.closers["indy-1-0-0.jpg"].written) + assert.Equal(t, 12050, testOutputter.closers["indy-1-1-0.jpg"].written) + assert.Equal(t, 2722, testOutputter.closers["indy-1-0-1.jpg"].written) + assert.Equal(t, 2477, testOutputter.closers["indy-1-1-1.jpg"].written) + +} diff --git a/testdata/gold.jpg b/testdata/gold.jpg new file mode 100644 index 0000000..f1f16ca Binary files /dev/null and b/testdata/gold.jpg differ