img2pdf/main.go
Leak Technologies 465f90a8f8 Simplify project structure and update build system
- 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
2026-01-09 03:34:58 -05:00

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
}