2022-08-11 09:10:07 +00:00
|
|
|
// Copyright (c) 2019-2022 David Vogel
|
|
|
|
//
|
|
|
|
// This software is released under the MIT License.
|
|
|
|
// https://opensource.org/licenses/MIT
|
|
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"image"
|
2024-04-06 21:28:15 +00:00
|
|
|
"image/draw"
|
2022-08-11 09:10:07 +00:00
|
|
|
_ "image/png"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strconv"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/nfnt/resize"
|
|
|
|
)
|
|
|
|
|
|
|
|
var ImageTileFileRegex = regexp.MustCompile(`^(-?\d+),(-?\d+).png$`)
|
|
|
|
|
|
|
|
type ImageTile struct {
|
|
|
|
fileName string
|
|
|
|
modTime time.Time
|
|
|
|
|
|
|
|
scaleDivider int // Downscales the coordinates and images on the fly.
|
|
|
|
|
2022-08-12 09:39:55 +00:00
|
|
|
image image.Image // Either a rectangle or an RGBA image. The bounds of this image are determined by the filename.
|
|
|
|
imageMutex *sync.RWMutex
|
2022-08-11 23:06:22 +00:00
|
|
|
|
|
|
|
invalidationChan chan struct{} // Used to send invalidation requests to the tile's goroutine.
|
|
|
|
timeoutChan chan struct{} // Used to determine whether the tile is still being accessed or not.
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// NewImageTile returns an image tile object that represents the image at the given path.
|
2023-12-23 00:09:50 +00:00
|
|
|
// The filename will be used to determine the top left x and y coordinate of the tile.
|
2022-08-11 09:10:07 +00:00
|
|
|
// This will not load the image into RAM.
|
|
|
|
func NewImageTile(path string, scaleDivider int) (ImageTile, error) {
|
|
|
|
if scaleDivider < 1 {
|
|
|
|
return ImageTile{}, fmt.Errorf("invalid scale of %v", scaleDivider)
|
|
|
|
}
|
|
|
|
|
|
|
|
baseName := filepath.Base(path)
|
|
|
|
result := ImageTileFileRegex.FindStringSubmatch(baseName)
|
|
|
|
var x, y int
|
|
|
|
if parsed, err := strconv.ParseInt(result[1], 10, 0); err == nil {
|
|
|
|
x = int(parsed)
|
|
|
|
} else {
|
|
|
|
return ImageTile{}, fmt.Errorf("error parsing %q to integer: %w", result[1], err)
|
|
|
|
}
|
|
|
|
if parsed, err := strconv.ParseInt(result[2], 10, 0); err == nil {
|
|
|
|
y = int(parsed)
|
|
|
|
} else {
|
|
|
|
return ImageTile{}, fmt.Errorf("error parsing %q to integer: %w", result[2], err)
|
|
|
|
}
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
width, height, err := GetImageFileDimension(path)
|
2022-08-11 09:10:07 +00:00
|
|
|
if err != nil {
|
|
|
|
return ImageTile{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var modTime time.Time
|
|
|
|
fileInfo, err := os.Lstat(path)
|
|
|
|
if err == nil {
|
|
|
|
modTime = fileInfo.ModTime()
|
|
|
|
}
|
|
|
|
|
|
|
|
return ImageTile{
|
2022-08-11 23:06:22 +00:00
|
|
|
fileName: path,
|
|
|
|
modTime: modTime,
|
|
|
|
scaleDivider: scaleDivider,
|
2023-12-23 00:09:50 +00:00
|
|
|
image: image.Rect(DivideFloor(x, scaleDivider), DivideFloor(y, scaleDivider), DivideCeil(x+width, scaleDivider), DivideCeil(y+height, scaleDivider)),
|
2022-08-11 23:06:22 +00:00
|
|
|
imageMutex: &sync.RWMutex{},
|
|
|
|
invalidationChan: make(chan struct{}, 1),
|
|
|
|
timeoutChan: make(chan struct{}, 1),
|
2022-08-11 09:10:07 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetImage returns an image.Image that contains the tile pixel data.
|
|
|
|
// This will not return errors in case something went wrong, but will just return nil.
|
|
|
|
// All errors are written to stdout.
|
|
|
|
func (it *ImageTile) GetImage() *image.RGBA {
|
|
|
|
it.imageMutex.RLock()
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
// Clear the timeout chan to signal that the image is still being used.
|
|
|
|
select {
|
|
|
|
case <-it.timeoutChan:
|
|
|
|
default:
|
|
|
|
}
|
2022-08-11 09:10:07 +00:00
|
|
|
|
|
|
|
// Check if the image is already loaded.
|
|
|
|
if img, ok := it.image.(*image.RGBA); ok {
|
|
|
|
it.imageMutex.RUnlock()
|
|
|
|
return img
|
|
|
|
}
|
|
|
|
|
|
|
|
it.imageMutex.RUnlock()
|
|
|
|
// It's possible that the image got changed in between here.
|
|
|
|
it.imageMutex.Lock()
|
|
|
|
defer it.imageMutex.Unlock()
|
|
|
|
|
|
|
|
// Check again if the image is already loaded.
|
|
|
|
if img, ok := it.image.(*image.RGBA); ok {
|
|
|
|
return img
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store rectangle of the old image.
|
|
|
|
oldRect := it.image.Bounds()
|
|
|
|
|
|
|
|
file, err := os.Open(it.fileName)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Couldn't load file %q: %v.", it.fileName, err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
img, _, err := image.Decode(file)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("Couldn't decode image %q: %v.", it.fileName, err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if it.scaleDivider > 1 {
|
|
|
|
img = resize.Resize(uint(oldRect.Dx()), uint(oldRect.Dy()), img, resize.NearestNeighbor)
|
|
|
|
}
|
|
|
|
|
2024-04-06 21:28:15 +00:00
|
|
|
var imgRGBA *image.RGBA
|
|
|
|
switch img := img.(type) {
|
|
|
|
case *image.RGBA:
|
|
|
|
imgRGBA = img
|
|
|
|
case *image.NRGBA:
|
|
|
|
bounds := img.Bounds()
|
|
|
|
imgRGBA = image.NewRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy()))
|
|
|
|
draw.Draw(imgRGBA, imgRGBA.Bounds(), img, bounds.Min, draw.Src)
|
|
|
|
default:
|
|
|
|
log.Printf("Expected an RGBA or NRGBA image for %q, got %T instead.", it.fileName, img)
|
2022-08-11 09:10:07 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
imgRGBA.Rect = imgRGBA.Rect.Add(oldRect.Min)
|
|
|
|
|
|
|
|
it.image = imgRGBA
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
// Clear any old invalidation request.
|
|
|
|
select {
|
|
|
|
case <-it.invalidationChan:
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fill timeout channel with one element.
|
|
|
|
// This is needed, as the ticker doesn't send a tick on initialization.
|
|
|
|
select {
|
|
|
|
case it.timeoutChan <- struct{}{}:
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
// Free the image after some time or if requested externally.
|
2022-08-11 09:10:07 +00:00
|
|
|
go func() {
|
2022-08-11 23:06:22 +00:00
|
|
|
// Set up watchdog that checks if the image is being used.
|
|
|
|
ticker := time.NewTicker(5000 * time.Millisecond)
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
|
|
|
loop:
|
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ticker.C:
|
|
|
|
// Try to send to the timeout channel.
|
|
|
|
select {
|
|
|
|
case it.timeoutChan <- struct{}{}:
|
|
|
|
default:
|
|
|
|
// Timeout channel is full because the tile image wasn't queried recently.
|
|
|
|
break loop
|
|
|
|
}
|
|
|
|
case <-it.invalidationChan:
|
|
|
|
// An invalidation was requested externally.
|
|
|
|
break loop
|
|
|
|
}
|
2022-08-11 09:10:07 +00:00
|
|
|
}
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
// Free image and other stuff.
|
2022-08-11 09:10:07 +00:00
|
|
|
it.imageMutex.Lock()
|
|
|
|
defer it.imageMutex.Unlock()
|
|
|
|
it.image = it.image.Bounds()
|
|
|
|
}()
|
|
|
|
|
|
|
|
return imgRGBA
|
|
|
|
}
|
|
|
|
|
2022-08-11 23:06:22 +00:00
|
|
|
// Clears the cached image.
|
|
|
|
func (it *ImageTile) Invalidate() {
|
|
|
|
it.imageMutex.RLock()
|
|
|
|
defer it.imageMutex.RUnlock()
|
|
|
|
|
|
|
|
// Try to send invalidation request.
|
|
|
|
select {
|
|
|
|
case it.invalidationChan <- struct{}{}:
|
|
|
|
default:
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-11 09:10:07 +00:00
|
|
|
// The scaled image boundaries.
|
|
|
|
// This matches exactly to what GetImage() returns.
|
|
|
|
func (it *ImageTile) Bounds() image.Rectangle {
|
|
|
|
it.imageMutex.RLock()
|
|
|
|
defer it.imageMutex.RUnlock()
|
|
|
|
|
|
|
|
return it.image.Bounds()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (it *ImageTile) String() string {
|
|
|
|
return fmt.Sprintf("{ImageTile: %q}", it.fileName)
|
|
|
|
}
|