2022-07-16 15:29:26 +00:00
// Copyright (c) 2019-2022 David Vogel
2019-10-21 00:07:39 +00:00
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
package main
import (
2019-10-25 17:45:23 +00:00
"flag"
"fmt"
2019-10-23 01:28:37 +00:00
"image"
"image/png"
2019-10-21 00:07:39 +00:00
"log"
2019-10-23 01:28:37 +00:00
"os"
2019-10-21 00:07:39 +00:00
"path/filepath"
2019-11-05 01:31:19 +00:00
"sync"
"time"
2019-10-25 17:45:23 +00:00
2022-07-30 10:23:40 +00:00
"github.com/1lann/promptui"
2019-11-05 01:31:19 +00:00
"github.com/cheggaaa/pb/v3"
2019-10-21 00:07:39 +00:00
)
2019-10-25 17:45:23 +00:00
var flagInputPath = flag . String ( "input" , filepath . Join ( "." , ".." , ".." , "output" ) , "The source path of the image tiles to be stitched." )
2022-08-10 19:04:17 +00:00
var flagEntitiesInputPath = flag . String ( "entities" , filepath . Join ( "." , ".." , ".." , "output" , "entities.json" ) , "The path to the entities.json file." )
var flagPlayerPathInputPath = flag . String ( "player-path" , filepath . Join ( "." , ".." , ".." , "output" , "player-path.json" ) , "The path to the player-path.json file." )
2019-10-25 17:45:23 +00:00
var flagOutputPath = flag . String ( "output" , filepath . Join ( "." , "output.png" ) , "The path and filename of the resulting stitched image." )
2019-11-01 01:40:21 +00:00
var flagScaleDivider = flag . Int ( "divide" , 1 , "A downscaling factor. 2 will produce an image with half the side lengths." )
2022-08-11 23:06:22 +00:00
var flagBlendTileLimit = flag . Int ( "tile-limit" , 9 , "Limits median blending to the n newest tiles by file modification time. If set to 0, all available tiles will be median blended." )
2019-10-25 17:45:23 +00:00
var flagXMin = flag . Int ( "xmin" , 0 , "Left bound of the output rectangle. This coordinate is included in the output." )
var flagYMin = flag . Int ( "ymin" , 0 , "Upper bound of the output rectangle. This coordinate is included in the output." )
var flagXMax = flag . Int ( "xmax" , 0 , "Right bound of the output rectangle. This coordinate is not included in the output." )
var flagYMax = flag . Int ( "ymax" , 0 , "Lower bound of the output rectangle. This coordinate is not included in the output." )
2019-10-21 00:07:39 +00:00
func main ( ) {
2022-08-11 23:06:22 +00:00
log . Printf ( "Noita MapCapture stitching tool v%s." , version )
2022-07-16 21:22:18 +00:00
2019-10-25 17:45:23 +00:00
flag . Parse ( )
2022-08-11 09:10:07 +00:00
var overlays [ ] StitchedImageOverlay
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-10-25 17:45:23 +00:00
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter downscaling factor:" ,
Default : fmt . Sprint ( * flagScaleDivider ) ,
AllowEdit : true ,
Validate : func ( s string ) error {
var num int
_ , err := fmt . Sscanf ( s , "%d" , & num )
if err != nil {
return err
}
if int ( num ) < 1 {
2022-07-16 15:29:26 +00:00
return fmt . Errorf ( "number must be larger than 0" )
2019-10-25 17:45:23 +00:00
}
return nil
} ,
}
result , err := prompt . Run ( )
if err != nil {
2022-08-11 23:06:22 +00:00
log . Panicf ( "Error while getting user input: %v." , err )
2019-10-25 17:45:23 +00:00
}
fmt . Sscanf ( result , "%d" , flagScaleDivider )
}
2022-08-11 23:06:22 +00:00
// Query the user, if there were no cmd arguments given.
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter blend tile limit:" ,
Default : fmt . Sprint ( * flagBlendTileLimit ) ,
AllowEdit : true ,
Validate : func ( s string ) error {
var num int
_ , err := fmt . Sscanf ( s , "%d" , & num )
if err != nil {
return err
}
if int ( num ) < 0 {
return fmt . Errorf ( "number must be at least 0" )
}
return nil
} ,
}
result , err := prompt . Run ( )
if err != nil {
log . Panicf ( "Error while getting user input: %v." , err )
}
fmt . Sscanf ( result , "%d" , flagBlendTileLimit )
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-10-25 17:45:23 +00:00
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter input path:" ,
Default : * flagInputPath ,
AllowEdit : true ,
}
result , err := prompt . Run ( )
if err != nil {
2022-08-11 23:06:22 +00:00
log . Panicf ( "Error while getting user input: %v." , err )
2019-10-25 17:45:23 +00:00
}
* flagInputPath = result
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter \"entities.json\" path:" ,
Default : * flagEntitiesInputPath ,
AllowEdit : true ,
}
result , err := prompt . Run ( )
if err != nil {
2022-08-11 23:06:22 +00:00
log . Panicf ( "Error while getting user input: %v." , err )
2022-08-08 21:05:58 +00:00
}
* flagEntitiesInputPath = result
}
// Load entities if requested.
2022-08-11 09:10:07 +00:00
entities , err := LoadEntities ( * flagEntitiesInputPath )
2022-08-08 21:05:58 +00:00
if err != nil {
2022-08-11 23:06:22 +00:00
log . Printf ( "Failed to load entities: %v." , err )
2022-08-08 21:05:58 +00:00
}
if len ( entities ) > 0 {
log . Printf ( "Got %v entities." , len ( entities ) )
2022-08-11 09:10:07 +00:00
overlays = append ( overlays , entities ) // Add entities to overlay drawing list.
2022-08-08 21:05:58 +00:00
}
2022-08-10 18:41:57 +00:00
// Query the user, if there were no cmd arguments given.
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter \"player-path.json\" path:" ,
Default : * flagPlayerPathInputPath ,
AllowEdit : true ,
}
result , err := prompt . Run ( )
if err != nil {
2022-08-11 23:06:22 +00:00
log . Panicf ( "Error while getting user input: %v." , err )
2022-08-10 18:41:57 +00:00
}
* flagPlayerPathInputPath = result
}
// Load player path if requested.
2022-08-11 09:10:07 +00:00
playerPath , err := LoadPlayerPath ( * flagPlayerPathInputPath )
2022-08-10 18:41:57 +00:00
if err != nil {
2022-08-11 23:06:22 +00:00
log . Printf ( "Failed to load player path: %v." , err )
2022-08-10 18:41:57 +00:00
}
2022-08-11 09:10:07 +00:00
if len ( playerPath ) > 0 {
log . Printf ( "Got %v player path entries." , len ( playerPath ) )
overlays = append ( overlays , playerPath ) // Add player path to overlay drawing list.
2022-08-10 18:41:57 +00:00
}
2022-08-11 23:06:22 +00:00
log . Printf ( "Starting to read tile information at %q." , * flagInputPath )
2022-08-11 09:10:07 +00:00
tiles , err := LoadImageTiles ( * flagInputPath , * flagScaleDivider )
2019-10-21 00:07:39 +00:00
if err != nil {
log . Panic ( err )
}
2019-10-25 17:45:23 +00:00
if len ( tiles ) == 0 {
2022-08-11 23:06:22 +00:00
log . Panicf ( "Got no image tiles from %q." , * flagInputPath )
2019-10-25 17:45:23 +00:00
}
2022-08-11 23:06:22 +00:00
log . Printf ( "Got %v tiles." , len ( tiles ) )
2019-10-21 00:07:39 +00:00
2019-10-24 19:11:23 +00:00
totalBounds := image . Rectangle { }
for i , tile := range tiles {
if i == 0 {
totalBounds = tile . Bounds ( )
} else {
totalBounds = totalBounds . Union ( tile . Bounds ( ) )
}
}
2022-08-11 23:06:22 +00:00
log . Printf ( "Total size of the possible output space is %v." , totalBounds )
2019-10-23 01:28:37 +00:00
2022-08-08 21:05:58 +00:00
// If the output rect is empty, use the rectangle that encloses all tiles.
2019-10-25 17:45:23 +00:00
outputRect := image . Rect ( * flagXMin , * flagYMin , * flagXMax , * flagYMax )
if outputRect . Empty ( ) {
outputRect = totalBounds
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-10-25 17:45:23 +00:00
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter output rectangle (xMin,yMin;xMax,yMax):" ,
Default : fmt . Sprintf ( "%d,%d;%d,%d" , outputRect . Min . X , outputRect . Min . Y , outputRect . Max . X , outputRect . Max . Y ) ,
AllowEdit : true ,
Validate : func ( s string ) error {
var xMin , yMin , xMax , yMax int
_ , err := fmt . Sscanf ( s , "%d,%d;%d,%d" , & xMin , & yMin , & xMax , & yMax )
if err != nil {
return err
}
rect := image . Rect ( xMin , yMin , xMax , yMax )
if rect . Empty ( ) {
2022-07-16 15:29:26 +00:00
return fmt . Errorf ( "rectangle must not be empty" )
2019-10-25 17:45:23 +00:00
}
outputRect = rect
2019-10-24 01:05:42 +00:00
2019-10-25 17:45:23 +00:00
return nil
} ,
}
result , err := prompt . Run ( )
if err != nil {
2022-08-11 23:06:22 +00:00
log . Panicf ( "Error while getting user input: %v." , err )
2019-10-25 17:45:23 +00:00
}
var xMin , yMin , xMax , yMax int
fmt . Sscanf ( result , "%d,%d;%d,%d" , & xMin , & yMin , & xMax , & yMax )
outputRect = image . Rect ( xMin , yMin , xMax , yMax )
}
2022-08-08 21:05:58 +00:00
// Query the user, if there were no cmd arguments given.
2019-10-25 17:45:23 +00:00
if flag . NFlag ( ) == 0 {
prompt := promptui . Prompt {
Label : "Enter output filename and path:" ,
Default : * flagOutputPath ,
AllowEdit : true ,
}
result , err := prompt . Run ( )
if err != nil {
2022-08-11 23:06:22 +00:00
log . Panicf ( "Error while getting user input: %v." , err )
2019-10-25 17:45:23 +00:00
}
* flagOutputPath = result
}
2019-10-23 01:28:37 +00:00
2022-08-11 23:06:22 +00:00
startTime := time . Now ( )
2019-11-05 01:31:19 +00:00
bar := pb . Full . New ( 0 )
var wg sync . WaitGroup
2022-08-11 09:10:07 +00:00
done := make ( chan struct { } )
2019-11-05 01:31:19 +00:00
2022-08-11 09:47:18 +00:00
blendMethod := BlendMethodMedian {
2022-08-11 23:06:22 +00:00
BlendTileLimit : * flagBlendTileLimit , // Limit median blending to the n newest tiles by file modification time.
2022-08-11 09:47:18 +00:00
}
2022-08-11 23:06:22 +00:00
outputImage , err := NewStitchedImage ( tiles , outputRect , blendMethod , 64 , overlays )
2022-08-11 09:10:07 +00:00
if err != nil {
2022-08-11 23:06:22 +00:00
log . Panicf ( "NewStitchedImage() failed: %v." , err )
2022-08-11 09:10:07 +00:00
}
_ , max := outputImage . Progress ( )
2022-08-11 23:06:22 +00:00
bar . SetTotal ( int64 ( max ) ) . Start ( ) . SetRefreshRate ( 250 * time . Millisecond )
2022-08-10 19:04:17 +00:00
2022-08-11 09:10:07 +00:00
// Query progress and draw progress bar.
2022-08-10 19:04:17 +00:00
wg . Add ( 1 )
go func ( ) {
defer wg . Done ( )
2022-08-11 23:06:22 +00:00
ticker := time . NewTicker ( 250 * time . Millisecond )
2022-08-10 19:04:17 +00:00
for {
select {
case <- done :
2022-08-11 09:10:07 +00:00
value , _ := outputImage . Progress ( )
2022-08-10 19:04:17 +00:00
bar . SetCurrent ( int64 ( value ) )
bar . Finish ( )
return
case <- ticker . C :
2022-08-11 09:10:07 +00:00
value , _ := outputImage . Progress ( )
2022-08-10 19:04:17 +00:00
bar . SetCurrent ( int64 ( value ) )
2019-11-05 01:31:19 +00:00
}
2022-08-10 19:04:17 +00:00
}
} ( )
2019-11-04 21:44:35 +00:00
2022-08-11 23:06:22 +00:00
log . Printf ( "Creating output file %q." , * flagOutputPath )
2019-12-21 21:25:47 +00:00
f , err := os . Create ( * flagOutputPath )
2019-10-23 01:28:37 +00:00
if err != nil {
log . Panic ( err )
}
2022-08-11 23:06:22 +00:00
encoder := png . Encoder {
CompressionLevel : png . DefaultCompression ,
}
if err := encoder . Encode ( f , outputImage ) ; err != nil {
2019-10-23 01:28:37 +00:00
f . Close ( )
log . Panic ( err )
}
2022-08-11 09:10:07 +00:00
done <- struct { } { }
2022-08-10 19:04:17 +00:00
wg . Wait ( )
2019-11-05 01:31:19 +00:00
2019-10-23 01:28:37 +00:00
if err := f . Close ( ) ; err != nil {
log . Panic ( err )
2019-10-21 00:07:39 +00:00
}
2022-08-11 23:06:22 +00:00
log . Printf ( "Created output file %q in %v." , * flagOutputPath , time . Since ( startTime ) )
2019-10-23 22:28:22 +00:00
2022-08-11 23:06:22 +00:00
//fmt.Println("Press the enter key to terminate the console screen!")
//fmt.Scanln()
2019-10-21 00:07:39 +00:00
}