2023-12-23 00:15:05 +00:00
|
|
|
// Copyright (c) 2022-2023 David Vogel
|
2022-08-11 09:10:07 +00:00
|
|
|
//
|
|
|
|
// This software is released under the MIT License.
|
|
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"image"
|
|
|
|
"image/color"
|
2022-08-12 09:39:55 +00:00
|
|
|
"sync/atomic"
|
2023-12-23 00:15:05 +00:00
|
|
|
"time"
|
2022-08-11 09:10:07 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
// StitchedImageCacheGridSize defines the worker chunk size when the cache image is regenerated.
|
2022-08-11 23:06:22 +00:00
|
|
|
var StitchedImageCacheGridSize = 256
|
2022-08-11 09:10:07 +00:00
|
|
|
|
2022-08-11 09:47:18 +00:00
|
|
|
// StitchedImageBlendMethod defines how tiles are blended together.
|
|
|
|
type StitchedImageBlendMethod interface {
|
|
|
|
Draw(tiles []*ImageTile, destImage *image.RGBA) // Draw is called when a new cache image is generated.
|
|
|
|
}
|
2022-08-11 09:10:07 +00:00
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
// StitchedImageOverlay defines an interface for arbitrary overlays that can be drawn over the stitched image.
|
2022-08-11 09:10:07 +00:00
|
|
|
type StitchedImageOverlay interface {
|
|
|
|
Draw(*image.RGBA)
|
|
|
|
}
|
|
|
|
|
|
|
|
// StitchedImage combines several ImageTile objects into a single RGBA image.
|
|
|
|
// The way the images are combined/blended is defined by the blendFunc.
|
|
|
|
type StitchedImage struct {
|
2022-08-11 23:06:22 +00:00
|
|
|
tiles ImageTiles
|
2022-08-11 09:47:18 +00:00
|
|
|
bounds image.Rectangle
|
|
|
|
blendMethod StitchedImageBlendMethod
|
|
|
|
overlays []StitchedImageOverlay
|
2022-08-11 09:10:07 +00:00
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
cacheRowHeight int
|
|
|
|
cacheRows []StitchedImageCache
|
|
|
|
cacheRowYOffset int // Defines the pixel offset of the first cache row.
|
2022-08-11 09:10:07 +00:00
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
oldCacheRowIndex int
|
2022-08-12 09:39:55 +00:00
|
|
|
queryCounter atomic.Int64
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewStitchedImage creates a new image from several single image tiles.
|
2022-08-11 23:06:22 +00:00
|
|
|
func NewStitchedImage(tiles ImageTiles, bounds image.Rectangle, blendMethod StitchedImageBlendMethod, cacheRowHeight int, overlays []StitchedImageOverlay) (*StitchedImage, error) {
|
2022-08-11 09:10:07 +00:00
|
|
|
if bounds.Empty() {
|
|
|
|
return nil, fmt.Errorf("given boundaries are empty")
|
|
|
|
}
|
2022-08-11 09:47:18 +00:00
|
|
|
if blendMethod == nil {
|
|
|
|
return nil, fmt.Errorf("no blending method given")
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
2022-08-11 23:06:22 +00:00
|
|
|
if cacheRowHeight <= 0 {
|
|
|
|
return nil, fmt.Errorf("invalid cache row height of %d pixels", cacheRowHeight)
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
stitchedImage := &StitchedImage{
|
2022-08-11 09:10:07 +00:00
|
|
|
tiles: tiles,
|
|
|
|
bounds: bounds,
|
2022-08-11 09:47:18 +00:00
|
|
|
blendMethod: blendMethod,
|
2022-08-11 09:10:07 +00:00
|
|
|
overlays: overlays,
|
2022-08-11 23:06:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Generate cache image rows.
|
2023-12-23 00:15:36 +00:00
|
|
|
maxRow := (bounds.Dy() - 1) / cacheRowHeight
|
2022-08-11 23:06:22 +00:00
|
|
|
var cacheRows []StitchedImageCache
|
2023-12-23 00:15:36 +00:00
|
|
|
for i := 0; i <= maxRow; i++ {
|
2022-08-11 23:06:22 +00:00
|
|
|
rect := image.Rect(bounds.Min.X, bounds.Min.Y+i*cacheRowHeight, bounds.Max.X, bounds.Min.Y+(i+1)*cacheRowHeight)
|
|
|
|
cacheRows = append(cacheRows, NewStitchedImageCache(stitchedImage, rect.Intersect(bounds)))
|
|
|
|
}
|
|
|
|
stitchedImage.cacheRowHeight = cacheRowHeight
|
|
|
|
stitchedImage.cacheRowYOffset = -bounds.Min.Y
|
|
|
|
stitchedImage.cacheRows = cacheRows
|
|
|
|
|
2023-12-23 00:15:05 +00:00
|
|
|
// Start ticker to automatically invalidate caches.
|
|
|
|
// Due to this, the stitchedImage object is not composable, as this goroutine will always have a reference.
|
|
|
|
go func() {
|
|
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
|
|
for range ticker.C {
|
|
|
|
for rowIndex := range stitchedImage.cacheRows {
|
|
|
|
stitchedImage.cacheRows[rowIndex].InvalidateAuto(3) // Invalidate cache row after 3 seconds of being idle.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
return stitchedImage, nil
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ColorModel returns the Image's color model.
|
|
|
|
func (si *StitchedImage) ColorModel() color.Model {
|
|
|
|
return color.RGBAModel
|
|
|
|
}
|
|
|
|
|
|
|
|
// Bounds returns the domain for which At can return non-zero color.
|
|
|
|
// The bounds do not necessarily contain the point (0, 0).
|
|
|
|
func (si *StitchedImage) Bounds() image.Rectangle {
|
|
|
|
return si.bounds
|
|
|
|
}
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
func (si *StitchedImage) At(x, y int) color.Color {
|
|
|
|
return si.RGBAAt(x, y)
|
|
|
|
}
|
|
|
|
|
2022-08-11 09:10:07 +00:00
|
|
|
// At returns the color of the pixel at (x, y).
|
|
|
|
//
|
|
|
|
// This is optimized to be read line by line (scanning), it will be much slower with random access.
|
|
|
|
//
|
|
|
|
// For the `Progress()` method to work correctly, every pixel should be queried exactly once.
|
|
|
|
//
|
|
|
|
// At(Bounds().Min.X, Bounds().Min.Y) // returns the top-left pixel of the image.
|
|
|
|
// At(Bounds().Max.X-1, Bounds().Max.Y-1) // returns the bottom-right pixel.
|
2022-08-11 23:06:22 +00:00
|
|
|
func (si *StitchedImage) RGBAAt(x, y int) color.RGBA {
|
2022-08-11 09:10:07 +00:00
|
|
|
// Assume that every pixel is only queried once.
|
2022-08-12 09:39:55 +00:00
|
|
|
si.queryCounter.Add(1)
|
2022-08-11 09:10:07 +00:00
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
// Determine the cache rowIndex index.
|
|
|
|
rowIndex := (y + si.cacheRowYOffset) / si.cacheRowHeight
|
|
|
|
if rowIndex < 0 || rowIndex >= len(si.cacheRows) {
|
|
|
|
return color.RGBA{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if we advanced/changed the row index.
|
|
|
|
// This doesn't happen a lot, so stuff inside this can be a bit more expensive.
|
|
|
|
if si.oldCacheRowIndex != rowIndex {
|
|
|
|
// Pre generate the new row asynchronously.
|
|
|
|
newRowIndex := rowIndex + 1
|
|
|
|
if newRowIndex >= 0 && newRowIndex < len(si.cacheRows) {
|
|
|
|
go si.cacheRows[newRowIndex].Regenerate()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Invalidate all tiles that are above the next row.
|
|
|
|
si.tiles.InvalidateAboveY((rowIndex+1)*si.cacheRowHeight - si.cacheRowYOffset)
|
2022-08-11 09:10:07 +00:00
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
si.oldCacheRowIndex = rowIndex
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
return si.cacheRows[rowIndex].RGBAAt(x, y)
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Opaque returns whether the image is fully opaque.
|
|
|
|
//
|
|
|
|
// For more speed and smaller file size, StitchedImage will be marked as non-transparent.
|
2023-12-23 00:16:25 +00:00
|
|
|
// This will speed up image saving by 2x, as there is no need to iterate over the whole image just to find a single non opaque pixel.
|
2022-08-11 09:10:07 +00:00
|
|
|
func (si *StitchedImage) Opaque() bool {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
// Progress returns the approximate progress of any process that scans the image from top to bottom.
|
|
|
|
func (si *StitchedImage) Progress() (value, max int) {
|
|
|
|
size := si.Bounds().Size()
|
|
|
|
|
2022-08-12 09:39:55 +00:00
|
|
|
return int(si.queryCounter.Load()), size.X * size.Y
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
2023-12-23 00:16:25 +00:00
|
|
|
|
|
|
|
// SubStitchedImage returns an image representing the portion of the image p visible through r.
|
|
|
|
// The returned image references to the original stitched image, and therefore reuses its cache.
|
|
|
|
func (si *StitchedImage) SubStitchedImage(r image.Rectangle) SubStitchedImage {
|
|
|
|
return SubStitchedImage{
|
|
|
|
StitchedImage: si,
|
|
|
|
bounds: si.Bounds().Intersect(r),
|
|
|
|
}
|
|
|
|
}
|