noita-mapcap/bin/stitch/dzi.go

158 lines
5.0 KiB
Go

// Copyright (c) 2023-2024 David Vogel
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package main
import (
"encoding/json"
"fmt"
"image"
"log"
"os"
"path/filepath"
"strconv"
"sync"
"time"
)
type DZI struct {
stitchedImage *StitchedImage
fileExtension string
tileSize int // The (maximum) width and height of a tile in pixels, not including the overlap.
overlap int // The amount of additional pixels on every side of every tile. The real (max) width/height of an image is `2*overlap + tileSize`.
maxZoomLevel int // The maximum zoom level that is needed.
}
// NewDZI creates a new DZI from the given StitchedImages.
//
// dziTileSize and dziOverlap define the size and overlap of the resulting DZI tiles.
func NewDZI(stitchedImage *StitchedImage, dziTileSize, dziOverlap int) DZI {
dzi := DZI{
stitchedImage: stitchedImage,
fileExtension: ".webp",
overlap: dziOverlap,
tileSize: dziTileSize,
}
width, height := stitchedImage.bounds.Dx(), stitchedImage.bounds.Dy()
// Calculate max zoom level and stuff.
neededLength := max(width, height)
var sideLength int = 1
var level int
for sideLength < neededLength {
level += 1
sideLength *= 2
}
dzi.maxZoomLevel = level
//dzi.maxZoomLevelLength = sideLength
return dzi
}
// ExportDZIDescriptor exports the descriptive JSON file at the given path.
func (d DZI) ExportDZIDescriptor(outputPath string) error {
log.Printf("Creating DZI descriptor %q.", outputPath)
f, err := os.Create(outputPath)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
defer f.Close()
// Prepare data that describes the layout of the image files.
var dziDescriptor struct {
Image struct {
XMLNS string `json:"xmlns"`
Format string
Overlap string
TileSize string
Size struct {
Width string
Height string
}
TopLeft struct {
X string
Y string
}
}
}
dziDescriptor.Image.XMLNS = "http://schemas.microsoft.com/deepzoom/2008"
dziDescriptor.Image.Format = "webp"
dziDescriptor.Image.Overlap = strconv.Itoa(d.overlap)
dziDescriptor.Image.TileSize = strconv.Itoa(d.tileSize)
dziDescriptor.Image.Size.Width = strconv.Itoa(d.stitchedImage.bounds.Dx())
dziDescriptor.Image.Size.Height = strconv.Itoa(d.stitchedImage.bounds.Dy())
dziDescriptor.Image.TopLeft.X = strconv.Itoa(d.stitchedImage.bounds.Min.X)
dziDescriptor.Image.TopLeft.Y = strconv.Itoa(d.stitchedImage.bounds.Min.Y)
jsonEnc := json.NewEncoder(f)
return jsonEnc.Encode(dziDescriptor)
}
// ExportDZITiles exports the single image tiles for every zoom level.
func (d DZI) ExportDZITiles(outputDir string) error {
log.Printf("Creating DZI tiles in %q.", outputDir)
// Start with the highest zoom level (Where every world pixel is exactly mapped into one image pixel).
// Generate all tiles for this level, and then stitch another image (scaled down by a factor of 2) based on the previously generated tiles.
// Repeat this process until we have generated level 0.
// The current stitched image we are working with.
stitchedImage := d.stitchedImage
for zoomLevel := d.maxZoomLevel; zoomLevel >= 0; zoomLevel-- {
levelBasePath := filepath.Join(outputDir, fmt.Sprintf("%d", zoomLevel))
if err := os.MkdirAll(levelBasePath, 0755); err != nil {
return fmt.Errorf("failed to create zoom level base directory %q: %w", levelBasePath, err)
}
// Store list of tiles, so that we can reuse them in the next step for the smaller zoom level.
imageTiles := ImageTiles{}
// Export tiles.
for iY := 0; iY <= (stitchedImage.bounds.Dy()-1)/d.tileSize; iY++ {
for iX := 0; iX <= (stitchedImage.bounds.Dx()-1)/d.tileSize; iX++ {
rect := image.Rect(iX*d.tileSize, iY*d.tileSize, iX*d.tileSize+d.tileSize, iY*d.tileSize+d.tileSize)
rect = rect.Add(stitchedImage.bounds.Min)
rect = rect.Inset(-d.overlap)
img := stitchedImage.SubStitchedImage(rect)
filePath := filepath.Join(levelBasePath, fmt.Sprintf("%d_%d%s", iX, iY, d.fileExtension))
if err := exportWebPSilent(img, filePath); err != nil {
return fmt.Errorf("failed to export WebP: %w", err)
}
scaleDivider := 2
imageTiles = append(imageTiles, ImageTile{
fileName: filePath,
modTime: time.Now(),
scaleDivider: scaleDivider,
image: image.Rect(DivideFloor(img.Bounds().Min.X, scaleDivider), DivideFloor(img.Bounds().Min.Y, scaleDivider), DivideCeil(img.Bounds().Max.X, scaleDivider), DivideCeil(img.Bounds().Max.Y, scaleDivider)),
imageMutex: &sync.RWMutex{},
invalidationChan: make(chan struct{}, 1),
timeoutChan: make(chan struct{}, 1),
})
}
}
// Create new stitched image from the previously exported tiles.
// The tiles are already created in a way, that they are scaled down by a factor of 2.
var err error
stitchedImage, err = NewStitchedImage(imageTiles, imageTiles.Bounds(), BlendMethodMedian{BlendTileLimit: 0}, 128, nil)
if err != nil {
return fmt.Errorf("failed to run NewStitchedImage(): %w", err)
}
}
return nil
}