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 }