- Remove Python codebase and packaging files - Consolidate Go application to single main.go file - Add Makefile for build management - Update README with new Go-only structure - Remove unused dependencies and legacy scripts
273 lines
6.0 KiB
Go
273 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
"image/png"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/app"
|
|
"fyne.io/fyne/v2/canvas"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/storage"
|
|
"fyne.io/fyne/v2/theme"
|
|
"fyne.io/fyne/v2/widget"
|
|
"github.com/jung-kurt/gofpdf"
|
|
)
|
|
|
|
var (
|
|
CreamMain = color.NRGBA{0xE6, 0xE1, 0xD6, 0xFF}
|
|
CreamInset = color.NRGBA{0xE3, 0xDD, 0xCF, 0xFF}
|
|
InkPrimary = color.NRGBA{0x1E, 0x1F, 0x22, 0xFF}
|
|
InkSoft = color.NRGBA{0x2A, 0x2B, 0x2F, 0xFF}
|
|
)
|
|
|
|
const dropPrompt = "Drag and drop files to\nbatch convert to PDF."
|
|
|
|
func main() {
|
|
a := app.New()
|
|
w := a.NewWindow("img2pdf")
|
|
|
|
w.Resize(fyne.NewSize(420, 380))
|
|
w.SetFixedSize(true)
|
|
|
|
// Root background
|
|
bg := canvas.NewRectangle(CreamMain)
|
|
|
|
// Drop area
|
|
dropBg := canvas.NewRectangle(CreamInset)
|
|
dropBg.CornerRadius = 10
|
|
|
|
dropBorder := canvas.NewRectangle(color.Transparent)
|
|
dropBorder.StrokeColor = InkPrimary
|
|
dropBorder.StrokeWidth = 2
|
|
dropBorder.CornerRadius = 10
|
|
|
|
folderIcon := widget.NewIcon(theme.FolderIcon())
|
|
|
|
dropText := canvas.NewText(dropPrompt, InkPrimary)
|
|
dropText.TextSize = 11
|
|
dropText.Alignment = fyne.TextAlignCenter
|
|
|
|
dropContent := container.NewVBox(
|
|
folderIcon,
|
|
dropText,
|
|
)
|
|
|
|
dropArea := container.NewStack(
|
|
dropBorder,
|
|
container.NewPadded(
|
|
dropBg,
|
|
container.NewCenter(dropContent),
|
|
),
|
|
)
|
|
|
|
// Output bar
|
|
pathBg := canvas.NewRectangle(CreamInset)
|
|
pathBg.CornerRadius = 5
|
|
pathEntry := widget.NewEntry()
|
|
pathEntry.SetText(defaultOutputPath())
|
|
|
|
browseBtn := widget.NewButton("Browse", func() {
|
|
saveDialog := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) {
|
|
if err != nil {
|
|
dialog.ShowError(err, w)
|
|
return
|
|
}
|
|
if uc == nil {
|
|
return
|
|
}
|
|
path := uc.URI().Path()
|
|
_ = uc.Close()
|
|
pathEntry.SetText(ensurePDFExtension(path))
|
|
}, w)
|
|
saveDialog.SetFileName(filepath.Base(pathEntry.Text))
|
|
saveDialog.SetFilter(storage.NewExtensionFileFilter([]string{".pdf"}))
|
|
saveDialog.Show()
|
|
})
|
|
browseBtn.Importance = widget.LowImportance
|
|
|
|
pathBar := container.NewStack(
|
|
pathBg,
|
|
container.NewPadded(
|
|
container.NewBorder(nil, nil, nil, browseBtn, pathEntry),
|
|
),
|
|
)
|
|
|
|
// Layout stack
|
|
content := container.NewVBox(
|
|
layout.NewSpacer(),
|
|
dropArea,
|
|
layout.NewSpacer(),
|
|
pathBar,
|
|
)
|
|
|
|
padded := container.NewPadded(content)
|
|
root := container.NewStack(bg, padded)
|
|
|
|
w.SetContent(root)
|
|
|
|
updateStatus := func(text string) {
|
|
fyne.CurrentApp().Driver().RunOnMain(func() {
|
|
dropText.Text = text
|
|
dropText.Refresh()
|
|
})
|
|
}
|
|
|
|
w.SetOnDropped(func(_ fyne.Position, uris []fyne.URI) {
|
|
paths := uriPaths(uris)
|
|
if len(paths) == 0 {
|
|
dialog.ShowInformation("No Files", "Drop one or more image files.", w)
|
|
return
|
|
}
|
|
|
|
outputPath := expandUser(pathEntry.Text)
|
|
if outputPath == "" {
|
|
outputPath = defaultOutputPath()
|
|
pathEntry.SetText(outputPath)
|
|
}
|
|
|
|
updateStatus("Converting images...")
|
|
go func() {
|
|
err := convertImagesToPDF(paths, outputPath)
|
|
if err != nil {
|
|
updateStatus(dropPrompt)
|
|
fyne.CurrentApp().Driver().RunOnMain(func() {
|
|
dialog.ShowError(err, w)
|
|
})
|
|
return
|
|
}
|
|
updateStatus(dropPrompt)
|
|
fyne.CurrentApp().Driver().RunOnMain(func() {
|
|
dialog.ShowInformation("Done", fmt.Sprintf("Saved PDF to:\n%s", outputPath), w)
|
|
})
|
|
}()
|
|
})
|
|
|
|
w.ShowAndRun()
|
|
}
|
|
|
|
func defaultOutputPath() string {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil || home == "" {
|
|
return "magazine.pdf"
|
|
}
|
|
return filepath.Join(home, "Documents", "img2pdf", "magazine.pdf")
|
|
}
|
|
|
|
func ensurePDFExtension(path string) string {
|
|
if strings.EqualFold(filepath.Ext(path), ".pdf") {
|
|
return path
|
|
}
|
|
return path + ".pdf"
|
|
}
|
|
|
|
func expandUser(path string) string {
|
|
path = strings.TrimSpace(path)
|
|
if path == "" {
|
|
return ""
|
|
}
|
|
if path == "~" {
|
|
home, err := os.UserHomeDir()
|
|
if err == nil {
|
|
return home
|
|
}
|
|
return path
|
|
}
|
|
if strings.HasPrefix(path, "~"+string(os.PathSeparator)) {
|
|
home, err := os.UserHomeDir()
|
|
if err == nil {
|
|
return filepath.Join(home, path[2:])
|
|
}
|
|
}
|
|
return path
|
|
}
|
|
|
|
func uriPaths(uris []fyne.URI) []string {
|
|
paths := make([]string, 0, len(uris))
|
|
for _, uri := range uris {
|
|
if uri == nil || uri.Scheme() != "file" {
|
|
continue
|
|
}
|
|
path := uri.Path()
|
|
if path == "" {
|
|
continue
|
|
}
|
|
paths = append(paths, path)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func convertImagesToPDF(paths []string, outputPath string) error {
|
|
if len(paths) == 0 {
|
|
return errors.New("no image files provided")
|
|
}
|
|
outputPath = ensurePDFExtension(expandUser(outputPath))
|
|
if outputPath == "" {
|
|
return errors.New("output path is empty")
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
|
return fmt.Errorf("create output folder: %w", err)
|
|
}
|
|
|
|
pdf := gofpdf.New("P", "pt", "A4", "")
|
|
pdf.SetMargins(0, 0, 0)
|
|
pdf.SetAutoPageBreak(false, 0)
|
|
|
|
for i, path := range paths {
|
|
img, err := loadImage(path)
|
|
if err != nil {
|
|
return fmt.Errorf("decode %s: %w", filepath.Base(path), err)
|
|
}
|
|
|
|
bounds := img.Bounds()
|
|
if bounds.Dx() == 0 || bounds.Dy() == 0 {
|
|
return fmt.Errorf("image has no size: %s", filepath.Base(path))
|
|
}
|
|
|
|
buf := &bytes.Buffer{}
|
|
if err := png.Encode(buf, img); err != nil {
|
|
return fmt.Errorf("encode %s: %w", filepath.Base(path), err)
|
|
}
|
|
|
|
name := fmt.Sprintf("img-%d", i)
|
|
opts := gofpdf.ImageOptions{ImageType: "PNG", ReadDpi: true}
|
|
if _, err := pdf.RegisterImageOptionsReader(name, opts, buf); err != nil {
|
|
return fmt.Errorf("register %s: %w", filepath.Base(path), err)
|
|
}
|
|
|
|
pageSize := gofpdf.SizeType{Wd: float64(bounds.Dx()), Ht: float64(bounds.Dy())}
|
|
pdf.AddPageFormat("P", pageSize)
|
|
pdf.ImageOptions(name, 0, 0, pageSize.Wd, pageSize.Ht, false, opts, 0, "")
|
|
}
|
|
|
|
if err := pdf.OutputFileAndClose(outputPath); err != nil {
|
|
return fmt.Errorf("write pdf: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadImage(path string) (image.Image, error) {
|
|
file, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
img, _, err := image.Decode(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return img, nil
|
|
}
|