export image functionality

This commit is contained in:
Grimsace
2026-02-02 12:24:14 -06:00
parent 1f66a8734c
commit 9ac036f2bd
4 changed files with 119 additions and 7 deletions
+3 -2
View File
@@ -5,6 +5,7 @@ go 1.25.6
require ( require (
fyne.io/fyne/v2 v2.7.2 fyne.io/fyne/v2 v2.7.2
github.com/aquilax/go-perlin v1.1.0 github.com/aquilax/go-perlin v1.1.0
github.com/chai2010/webp v1.4.0
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/ojrac/opensimplex-go v1.0.2 github.com/ojrac/opensimplex-go v1.0.2
) )
@@ -37,9 +38,9 @@ require (
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.7.8 // indirect github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect golang.org/x/image v0.35.0 // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
+6 -4
View File
@@ -6,6 +6,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/aquilax/go-perlin v1.1.0 h1:Gg+3jQ24wT4Y5GI7TCRLmYarzUG0k+n/JATFqOimb7s= github.com/aquilax/go-perlin v1.1.0 h1:Gg+3jQ24wT4Y5GI7TCRLmYarzUG0k+n/JATFqOimb7s=
github.com/aquilax/go-perlin v1.1.0/go.mod h1:z9Rl7EM4BZY0Ikp2fEN1I5mKSOJ26HQpk0O2TBdN2HE= github.com/aquilax/go-perlin v1.1.0/go.mod h1:z9Rl7EM4BZY0Ikp2fEN1I5mKSOJ26HQpk0O2TBdN2HE=
github.com/chai2010/webp v1.4.0 h1:6DA2pkkRUPnbOHvvsmGI3He1hBKf/bkRlniAiSGuEko=
github.com/chai2010/webp v1.4.0/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -72,15 +74,15 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+95 -1
View File
@@ -12,15 +12,21 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"image/jpeg"
"strings"
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/app" "fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"github.com/chai2010/webp"
) )
func main() { func main() {
a := app.New() a := app.NewWithID("com.example.rpgcitymaker")
a.Settings().SetTheme(&CustomTheme{a.Settings().Theme()}) a.Settings().SetTheme(&CustomTheme{a.Settings().Theme()})
w := a.NewWindow("RPG City Maker") w := a.NewWindow("RPG City Maker")
w.Resize(fyne.NewSize(800, 600)) w.Resize(fyne.NewSize(800, 600))
@@ -254,6 +260,9 @@ func main() {
var generateBtn *widget.Button var generateBtn *widget.Button
progressBar := NewTextOverlayProgressBar() progressBar := NewTextOverlayProgressBar()
exportBtn := widget.NewButton("Export", func() {
showSaveDialog(w, canvasImg.Image, settings)
})
generateBtn = widget.NewButton("Generate", func() { generateBtn = widget.NewButton("Generate", func() {
go func() { go func() {
@@ -437,6 +446,8 @@ func main() {
generateBtn, generateBtn,
errorLabel, errorLabel,
progressBar, progressBar,
widget.NewSeparator(),
exportBtn,
)) ))
tabs := container.NewAppTabs( tabs := container.NewAppTabs(
@@ -464,3 +475,86 @@ func main() {
w.SetContent(split) w.SetContent(split)
w.ShowAndRun() w.ShowAndRun()
} }
func showSaveDialog(win fyne.Window, img image.Image, settings *Settings) {
fileNameEntry := widget.NewEntry()
fileNameEntry.SetPlaceHolder("image")
formatSelect := widget.NewSelect([]string{"PNG", "JPG", "WEBP"}, nil)
formatSelect.SetSelected("PNG")
pathLabel := widget.NewLabel(settings.LastExportPath)
browseBtn := widget.NewButton("Browse", func() {
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
if err != nil {
log.Println("Error opening folder dialog:", err)
return
}
if uri == nil {
return
}
settings.LastExportPath = uri.Path()
pathLabel.SetText(settings.LastExportPath)
}, win)
})
content := container.NewVBox(
widget.NewLabel("Save to:"),
container.NewBorder(nil, nil, nil, browseBtn, pathLabel),
widget.NewLabel("Filename:"),
fileNameEntry,
widget.NewLabel("Format:"),
formatSelect,
)
saveDialog := dialog.NewCustom("Export Image", "Cancel", content, win)
saveBtn := widget.NewButton("Save", func() {
fileName := fileNameEntry.Text
if fileName == "" {
return
}
filePath := filepath.Join(pathLabel.Text, fileName+"."+strings.ToLower(formatSelect.Selected))
settings.LastExportPath = filepath.Dir(filePath)
file, err := os.Create(filePath)
if err != nil {
log.Println("Error creating file:", err)
return
}
defer file.Close()
switch formatSelect.Selected {
case "PNG":
err = png.Encode(file, img)
case "JPG":
err = jpeg.Encode(file, img, nil)
case "WEBP":
err = webp.Encode(file, img, &webp.Options{Lossless: true})
}
if err != nil {
log.Println("Failed to encode image:", err)
}
saveDialog.Hide()
})
saveBtn.Disable()
fileNameEntry.OnChanged = func(text string) {
if text == "" {
saveBtn.Disable()
} else {
saveBtn.Enable()
}
}
saveDialog.SetButtons([]fyne.CanvasObject{
widget.NewButton("Cancel", func() {
saveDialog.Hide()
}),
saveBtn,
})
saveDialog.Show()
}
+15
View File
@@ -24,6 +24,7 @@ type Settings struct {
MinRiverWidth float64 `json:"min_river_width"` MinRiverWidth float64 `json:"min_river_width"`
MaxRiverWidth float64 `json:"max_river_width"` MaxRiverWidth float64 `json:"max_river_width"`
RiverCurvyness float64 `json:"river_curvyness"` RiverCurvyness float64 `json:"river_curvyness"`
LastExportPath string `json:"last_export_path"`
} }
func (s *Settings) Save() error { func (s *Settings) Save() error {
@@ -58,6 +59,10 @@ func LoadSettings() (*Settings, error) {
file, err := os.Open(configFile) file, err := os.Open(configFile)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
homeDir, err := os.UserHomeDir()
if err != nil {
homeDir = "."
}
return &Settings{ return &Settings{
Detail: 1, Detail: 1,
Roughness: 0, Roughness: 0,
@@ -75,6 +80,7 @@ func LoadSettings() (*Settings, error) {
MinRiverWidth: 1, MinRiverWidth: 1,
MaxRiverWidth: 5, MaxRiverWidth: 5,
RiverCurvyness: 50, RiverCurvyness: 50,
LastExportPath: homeDir,
}, nil }, nil
} }
return nil, err return nil, err
@@ -86,5 +92,14 @@ func LoadSettings() (*Settings, error) {
if err := decoder.Decode(&settings); err != nil { if err := decoder.Decode(&settings); err != nil {
return nil, err return nil, err
} }
if settings.LastExportPath == "" {
homeDir, err := os.UserHomeDir()
if err != nil {
homeDir = "."
}
settings.LastExportPath = homeDir
}
return &settings, nil return &settings, nil
} }