Author: Pecezen
Platform: Windows
Language: Go
โ Tool Overview
This is a fully offline PDF merging and encryption tool.
- No registration.
- No file uploads.
- No technical background required.
In just a few simple steps, you can merge multiple PDF files into one and secure it with AES-256 encryption, ensuring your documents stay private and never leave your computer.
โญ Why Would You Need This Tool?
Have you ever run into situations like these?
- ๐ Too many PDF files โ emailing or uploading them is a hassle
- ๐ Contracts, IDs, or sensitive documents you donโt want to trust to online services
- ๐งพ A whole folder of scanned PDFs that need to be combined
- ๐ต Online tools with file size limits, login requirements, or painfully slow processing
This tool was built specifically to solve these problems.
๐ง Key Features at a Glance
๐ PDF Merging
- Merge multiple PDF files
- Add an entire folder at once
- Freely adjust the merge order
- Option to insert separator pages between documents
๐ PDF Encryption (AES-256)
- One-click encryption after merging
- Uses AES-256 encryption strength
- Real-time password strength indicator:
- โ Weak
- โ Fair
- โ Secure
๐ Clear and Transparent Processing
- Real-time progress display โ no guessing if itโs stuck
- After completion, the tool shows:
- Output file location
- Final file size
๐ก Why Does โOfflineโ Matter?
๐ Your files are never uploaded to any server
๐ No third-party platforms involved
๐ No logs, no backups, no analysis
๐ Your documents exist only on your local machine
This is especially important for:
- Contracts
- Identification documents
- Internal or confidential files
๐ฅ How to Use (Itโs Really Simple)
1๏ธโฃ Click โAdd PDFโ or โAdd Folderโ
2๏ธโฃ Adjust the order if needed
3๏ธโฃ Choose the output location and file name
4๏ธโฃ (Optional) Set an encryption password
5๏ธโฃ Click โStart Merge & Processโ โ Done
๐ฅ๏ธ User Interface

๐พ Download for Windows (Executable)
Download
The download is a compressed file.
Extraction password: pecezen.org
๐Source Code
package main
import (
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"unicode"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"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/pdfcpu/pdfcpu/pkg/api"
"github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model"
)
// ๅๅฅฝ่จญๅฎ Key
const prefLangKey = "user_language"
// I18n ็ฟป่ญฏ่ณๆ็ตๆง
type i18n struct {
windowTitle string
addBtn string
addFolderBtn string
removeBtn string
moveUp string
moveDown string
outPlaceholder string
chooseOut string
dividerCheck string
encryptCheck string
mergeBtn string
passwordPlace string
sectionMerge string
sectionEnc string
langLabel string
aboutBtn string
aboutTitle string
aboutContent string
msgSuccess string
msgErrMinFile string
msgErrNoOut string
msgErrNoPW string
outputSizeLabel string
strengthWeak string
strengthMedium string
strengthStrong string
processing string
// ๆฐๅข๏ผๅ ๅฏ/ไฟ่ญทๆชๆกๆ็คบ
msgEncryptedListHeader string
}
var translations = map[string]i18n{
"็น้ซไธญๆ": {
windowTitle: "PDF ๅไฝต่ๅ ๅฏๅทฅๅ
ท V1.0",
addBtn: "ๅ ๅ
ฅ PDF",
addFolderBtn: "ๅ ๅ
ฅ่ณๆๅคพ",
removeBtn: "็งป้ค",
moveUp: "ไธ็งป",
moveDown: "ไธ็งป",
outPlaceholder: "่ผธๅบๆชๅ (ๅฆ: merged.pdf)",
chooseOut: "ๅฒๅญไฝ็ฝฎ",
dividerCheck: "ๅจๆไปถ้ๆๅ
ฅๅ้้ ",
encryptCheck: "ๅไฝตๅพๅ็จ AES-256 ๅ ๅฏ",
mergeBtn: "้ๅงๅไฝต่่็",
passwordPlace: "่ซ่ผธๅ
ฅๅ ๅฏๅฏ็ขผ",
sectionMerge: "1. ่ผธๅบ่ๅไฝต้ธ้
",
sectionEnc: "2. ๅ ๅฏ่จญๅฎ",
langLabel: "่ช่จ:",
aboutBtn: "้ๆผ",
aboutTitle: "้ๆผๆญค็จๅผ",
aboutContent: "ไฝ่
: pecezen.org\nCopyright ยฉ 2026 pecezen.org. All rights reserved.\n\nใ็ฌฌไธๆนๅ
งๅฎน่ฒๆใ\nๆฌ็จๅผๆ ธๅฟ่็ๅผๆๆก็จ pdfcpu ้ๆบๅฐๆกใ\npdfcpu ไพๆ Apache License 2.0 ๆๆฌไฝฟ็จใ\nCopyright ยฉ 2026 pdfcpu authors.\n\nใๅ
่ฒฌ่ฒๆใ\nไฝ่
ไธๅฐๅ ไฝฟ็จๆฌ็จๅผๆ้ ๆ็ไปปไฝ่ณๆ้บๅคฑใๆๅฃๆๅ
ถไปๅพๆ่ฒ ่ฒฌใ",
msgSuccess: "ๆไฝๆๅ๏ผ",
msgErrMinFile: "่ซ่ณๅฐๅ ๅ
ฅ 2 ๅ PDF ๆชๆกใ",
msgErrNoOut: "่ซๅ
ๆๅฎ่ผธๅบไฝ็ฝฎใ",
msgErrNoPW: "ๅ็จๅ ๅฏๆ๏ผๅฟ
้ ่ผธๅ
ฅๅฏ็ขผใ",
outputSizeLabel: "่ผธๅบๆชๆกๅคงๅฐ: ",
strengthWeak: "โ ๅคชๅผฑ",
strengthMedium: "โ ๆฎ้",
strengthStrong: "โ
ๅฎๅ
จ",
processing: "ๆญฃๅจ่็ไธญ๏ผ่ซ็จๅ...",
// ๆฐๅข
msgEncryptedListHeader: "ไปฅไธๆชๆก็กๆณๅไฝต๏ผๅฏ่ฝๅทฒๅ ๅฏๆๅไฟ่ญท๏ผ๏ผ",
},
"English": {
windowTitle: "PDF Merge & Encrypt Tool V1.0",
addBtn: "Add PDF",
addFolderBtn: "Add Folder",
removeBtn: "Remove",
moveUp: "Up",
moveDown: "Down",
outPlaceholder: "Output name (e.g. merged.pdf)",
chooseOut: "Save As...",
dividerCheck: "Insert divider pages",
encryptCheck: "Enable AES-256 Encryption",
mergeBtn: "Run Process",
passwordPlace: "Enter Password",
sectionMerge: "1. Merge Settings",
sectionEnc: "2. Security Settings",
langLabel: "Language:",
aboutBtn: "About",
aboutTitle: "About",
aboutContent: "Author: pecezen.org\nCopyright ยฉ 2026 pecezen.org. All rights reserved.\n\n[Third-party Attribution]\nThis tool uses the pdfcpu library.\npdfcpu is licensed under the Apache License 2.0.\nCopyright ยฉ 2026 pdfcpu authors.\n\n[Disclaimer]\nThe author is not responsible for any data loss, damage, or other consequences caused by the use of this program.",
msgSuccess: "Success!",
msgErrMinFile: "Add at least 2 PDF files.",
msgErrNoOut: "Please specify output location.",
msgErrNoPW: "Password required for encryption.",
outputSizeLabel: "Output Size: ",
strengthWeak: "โ Too Weak",
strengthMedium: "โ Medium",
strengthStrong: "โ
Strong",
processing: "Processing, please wait...",
// ๆฐๅข
msgEncryptedListHeader: "The following file(s) cannot be merged (encrypted/protected):",
},
"ๆฅๆ": {
windowTitle: "PDF็ตๅใปๆๅทๅใใผใซ V1.0",
addBtn: "PDFใ่ฟฝๅ ",
addFolderBtn: "ใใฉใซใใ่ฟฝๅ ",
removeBtn: "ๅ้ค",
moveUp: "ไธใธ",
moveDown: "ไธใธ",
outPlaceholder: "ๅบๅใใกใคใซๅ",
chooseOut: "ไฟๅญๅ
",
dividerCheck: "ไปๅใใใผใธใๆๅ
ฅ",
encryptCheck: "AES-256ๆๅทๅใๆๅนใซใใ",
mergeBtn: "็ตๅใจๅฎ่ก",
passwordPlace: "ใในใฏใผใ",
sectionMerge: "1. ็ตๅ่จญๅฎ",
sectionEnc: "2. ใปใญใฅใชใใฃ่จญๅฎ",
langLabel: "่จ่ช:",
aboutBtn: "ๆ
ๅ ฑ",
aboutTitle: "ใใฎใขใใชใซใคใใฆ",
aboutContent: "่่
: pecezen.org\nCopyright ยฉ 2026 pecezen.org. All rights reserved.\n\nใใตใผใใใผใใฃ่ฃฝใฉใคใใฉใชใฎๅฃฐๆใ\nๆฌใใญใฐใฉใ ใฎใณใข่็ๅผๆใฏ pdfcpu ใๆก็จใใฆใใพใใ\npdfcpu ใฏ Apache License 2.0 ใซๅบใฅใใฆไฝฟ็จใใใฆใใพใใ\nCopyright ยฉ 2026 pdfcpu authors.\n\nใๅ
่ฒฌไบ้
ใ\nไฝ่
ใฏใๆฌใใญใฐใฉใ ใฎไฝฟ็จใซใใฃใฆ็ใใใใผใฟใฎ็ดๅคฑใ็ ดๆใใใฎไปใฎ็ตๆใซใคใใฆไธๅใฎ่ฒฌไปปใ่ฒ ใใพใใใ",
msgSuccess: "ๆๅใใพใใ๏ผ",
msgErrMinFile: "2ใคไปฅไธใฎPDFใใกใคใซใ่ฟฝๅ ใใฆใใ ใใใ",
msgErrNoOut: "ไฟๅญๅ
ใๆๅฎใใฆใใ ใใใ",
msgErrNoPW: "ใในใฏใผใใๅฟ
่ฆใงใใ",
outputSizeLabel: "ๅบๅใตใคใบ: ",
strengthWeak: "โ ๅผฑใ",
strengthMedium: "โ ๆฎ้",
strengthStrong: "โ
ๅผทๅบ",
processing: "ๅฆ็ไธญใใใฐใใใๅพ
ใกใใ ใใ...",
// ๆฐๅข
msgEncryptedListHeader: "ๆฌกใฎใใกใคใซใฏ็ตๅใงใใพใใ๏ผๆๅทๅ/ไฟ่ญทใใใฆใใพใ๏ผ:",
},
"Deutsch": {
windowTitle: "PDF Zusammenfรผhren & Verschlรผsseln V1.0",
addBtn: "PDF hinzufรผgen",
addFolderBtn: "Ordner hinzufรผgen",
removeBtn: "Entfernen",
moveUp: "Nach oben",
moveDown: "Nach unten",
outPlaceholder: "Dateiname fรผr Ausgabe",
chooseOut: "Speichern unter...",
dividerCheck: "Trennblรคtter einfรผgen",
encryptCheck: "AES-256-Verschlรผsselung aktivieren",
mergeBtn: "Prozess starten",
passwordPlace: "Passwort eingeben",
sectionMerge: "1. Zusammenfรผhren-Einstellungen",
sectionEnc: "2. Sicherheitseinstellungen",
langLabel: "Sprache:",
aboutBtn: "รber",
aboutTitle: "รber diese App",
aboutContent: "Autor: pecezen.org\nCopyright ยฉ 2026 pecezen.org. All rights reserved.\n\n[Dritthersteller-Attributierung]\nDieses Tool verwendet die pdfcpu-Bibliothek.\npdfcpu ist unter der Apache License 2.0 lizenziert.\nCopyright ยฉ 2026 pdfcpu authors.\n\n[Haftungsausschluss]\nDer Autor haftet nicht fรผr Datenverlust, Schรคden oder andere Folgen, die durch die Nutzung dieses Programms entstehen.",
msgSuccess: "Erfolg!",
msgErrMinFile: "Mindestens 2 PDF-Dateien hinzufรผgen.",
msgErrNoOut: "Bitte Ausgabeort angeben.",
msgErrNoPW: "Passwort fรผr Verschlรผsselung erforderlich.",
outputSizeLabel: "Ausgabegrรถรe: ",
strengthWeak: "โ Zu schwach",
strengthMedium: "โ Mittel",
strengthStrong: "โ
Stark",
processing: "Verarbeitung, bitte warten...",
// ๆฐๅข
msgEncryptedListHeader: "Folgende Datei(en) kรถnnen nicht zusammengefรผhrt werden (verschlรผsselt/geschรผtzt):",
},
}
func formatFileSize(size int64) string {
if size < 1024 {
return fmt.Sprintf("%d B", size)
} else if size < 1024*1024 {
return fmt.Sprintf("%.2f KB", float64(size)/1024)
}
return fmt.Sprintf("%.2f MB", float64(size)/(1024*1024))
}
func checkPasswordStrength(pw string, t i18n) string {
if len(pw) == 0 {
return ""
}
if len(pw) < 6 {
return t.strengthWeak
}
var hasNumber, hasUpper, hasSpecial bool
for _, char := range pw {
switch {
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if len(pw) >= 10 && hasNumber && hasUpper && hasSpecial {
return t.strengthStrong
}
return t.strengthMedium
}
func cleanPath(p string) string {
res := p
if runtime.GOOS == "windows" {
if len(res) > 3 && res[0] == '/' && res[2] == ':' {
res = res[1:]
}
}
res = filepath.FromSlash(res)
if absPath, err := filepath.Abs(res); err == nil {
return absPath
}
return res
}
// ๅฎๅ
จๅฐๅช้ค้ถไฝๅ
็ตๆชๆก๏ผ้ฟๅ
่ชคๅชๅทฒๆๅ
งๅฎน็ๆชๆก๏ผ
func removeIfEmpty(p string) {
if p == "" {
return
}
if fi, err := os.Stat(p); err == nil && fi.Size() == 0 {
_ = os.Remove(p)
}
}
func moveFile(src, dst string) error {
if _, err := os.Stat(dst); err == nil {
_ = os.Remove(dst)
}
if err := os.Rename(src, dst); err == nil {
return nil
}
// ่ทจ็ฃ็ขๆไธๅ่ฃ็ฝฎๆ๏ผๆน็จ copy + ๅช้ค
sf, err := os.Open(src)
if err != nil {
return err
}
defer sf.Close()
df, err := os.Create(dst)
if err != nil {
return err
}
defer df.Close()
if _, err = io.Copy(df, sf); err != nil {
return err
}
_ = sf.Close()
return os.Remove(src)
}
// ๅช่ค่ฃฝ๏ผไธๅชไพๆบ๏ผ
func copyFile(src, dst string) error {
if _, err := os.Stat(dst); err == nil {
_ = os.Remove(dst)
}
sf, err := os.Open(src)
if err != nil {
return err
}
defer sf.Close()
df, err := os.Create(dst)
if err != nil {
return err
}
defer df.Close()
_, err = io.Copy(df, sf)
return err
}
// ๅ่ฉฆๅคๆทๆชๆกๆฏๅฆ็บๅ ๅฏ/ๅไฟ่ญท๏ผๆไธๅฏ่ฎ๏ผ๏ผไปฅ้ฟๅ
ๅไฝตๆ็ดๆฅๅด้ฏใ
// ้่ฃกไปฅ Validate ๅ่ฉฆ๏ผ่ฅ้ฏ่ชค่จๆฏๅ
ๅซ encrypt/password/protect ็ญ้้ตๅญ๏ผๅฐฑ่ฆ็บ็กๆณๅไฝตใ
func isEncryptedOrProtected(path string) bool {
// ๅณๅ
ฅ nil ่ฎ pdfcpu ไฝฟ็จ้ ่จญ่จญๅฎ๏ผ็กๅฏ็ขผ๏ผ
err := api.ValidateFile(path, nil)
if err == nil {
return false
}
msg := strings.ToLower(err.Error())
if strings.Contains(msg, "encrypt") ||
strings.Contains(msg, "password") ||
strings.Contains(msg, "protected") ||
strings.Contains(msg, "permission") {
return true
}
// ๅ
ถไป validation ้ฏ่ชคไน่ฆ็บ็กๆณ่็๏ผ้ฟๅ
ๅพ็บ็ดๆฅๅด้ฏ
return true
}
func runPdfProcess(files []string, finalOut string, divider, encrypt bool, pw string) error {
// ๅฎๆชๆกไพ๏ผ่ฅ้ๅ ๅฏ๏ผ็ดๆฅๅ ๅฏๅฎๆช๏ผ่ฅไธๅ ๅฏ๏ผๅคๅฑค้่ผฏไธๆๅ
่จฑๅท่ก๏ผ็ถญๆๅ่ฆๅ๏ผ
if len(files) == 1 {
single := files[0]
if encrypt {
pw = strings.TrimSpace(pw)
if pw == "" {
return fmt.Errorf("Password required")
}
tempEnc, err := os.CreateTemp("", "pdf_enc_*.pdf")
if err != nil {
return err
}
encPath := tempEnc.Name()
tempEnc.Close()
defer os.Remove(encPath)
encConf := model.NewAESConfiguration(pw, pw, 256)
if err := api.EncryptFile(single, encPath, encConf); err != nil {
return fmt.Errorf("Encrypt Error: %v", err)
}
return moveFile(encPath, finalOut)
}
// ้ๅ ๅฏไธๅฎๆช๏ผ็่ซไธๅคๅฑคไธๆๅผๅซๅฐ้่ฃก๏ผ่ฅๅผๅซ๏ผไฟๅฎไฝๆณๆก่ค่ฃฝ
return copyFile(single, finalOut)
}
// ๅคๆชๆกไพ๏ผๅ
ๅไฝต๏ผๅ่ฆ้่ฆๅ ๅฏ
tempMerge, err := os.CreateTemp("", "pdf_merge_*.pdf")
if err != nil {
return err
}
mergePath := tempMerge.Name()
tempMerge.Close()
defer os.Remove(mergePath)
if err := api.MergeCreateFile(files, mergePath, divider, nil); err != nil {
return fmt.Errorf("Merge Error: %v", err)
}
sourceToMove := mergePath
if encrypt {
pw = strings.TrimSpace(pw)
if pw == "" {
return fmt.Errorf("Password required")
}
tempEnc, err := os.CreateTemp("", "pdf_enc_*.pdf")
if err != nil {
return err
}
encPath := tempEnc.Name()
tempEnc.Close()
defer os.Remove(encPath)
encConf := model.NewAESConfiguration(pw, pw, 256)
if err := api.EncryptFile(mergePath, encPath, encConf); err != nil {
return fmt.Errorf("Encrypt Error: %v", err)
}
sourceToMove = encPath
}
return moveFile(sourceToMove, finalOut)
}
func main() {
a := app.NewWithID("org.pecezen.pdftool.v1")
w := a.NewWindow("PDF Tool V1.0")
w.Resize(fyne.NewSize(850, 650))
// ่จญๅฎ่ฆ็ชๅ็คบ
if iconRes, err := fyne.LoadResourceFromPath("icon.png"); err == nil {
w.SetIcon(iconRes)
}
files := make([]string, 0, 16)
selectedIndex := -1
savedLang := a.Preferences().StringWithFallback(prefLangKey, "็น้ซไธญๆ")
currentLang := savedLang
outEntry := widget.NewEntry()
passwordEntry := widget.NewEntry()
passwordEntry.Password = true
passwordEntry.Disable()
strengthLabel := widget.NewLabel("")
sizeResultLabel := widget.NewLabel("")
progressIndicator := widget.NewProgressBarInfinite()
progressIndicator.Hide()
dividerCheck := widget.NewCheck("", nil)
encryptCheck := widget.NewCheck("", func(checked bool) {
if checked {
passwordEntry.Enable()
} else {
passwordEntry.Disable()
strengthLabel.SetText("")
}
})
passwordEntry.OnChanged = func(s string) {
if encryptCheck.Checked {
strengthLabel.SetText(checkPasswordStrength(s, translations[currentLang]))
}
}
list := widget.NewList(
func() int { return len(files) },
func() fyne.CanvasObject { return widget.NewLabel("template") },
func(i widget.ListItemID, o fyne.CanvasObject) {
o.(*widget.Label).SetText(fmt.Sprintf("%d. %s", i+1, filepath.Base(files[i])))
},
)
addBtn := widget.NewButtonWithIcon("", theme.ContentAddIcon(), nil)
addFolderBtn := widget.NewButtonWithIcon("", theme.FolderOpenIcon(), nil)
removeBtn := widget.NewButtonWithIcon("", theme.DeleteIcon(), nil)
upBtn := widget.NewButtonWithIcon("", theme.MoveUpIcon(), nil)
downBtn := widget.NewButtonWithIcon("", theme.MoveDownIcon(), nil)
chooseOutBtn := widget.NewButtonWithIcon("", theme.DocumentSaveIcon(), nil)
mergeBtn := widget.NewButtonWithIcon("", theme.ConfirmIcon(), nil)
aboutBtn := widget.NewButtonWithIcon("", theme.InfoIcon(), nil)
mergeLabel := widget.NewLabelWithStyle("", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
encryptLabel := widget.NewLabelWithStyle("", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
langLabelPrefix := widget.NewLabel("")
refreshUI := func(langKey string) {
currentLang = langKey
t := translations[langKey]
w.SetTitle(t.windowTitle)
addBtn.SetText(t.addBtn)
addFolderBtn.SetText(t.addFolderBtn)
removeBtn.SetText(t.removeBtn)
upBtn.SetText(t.moveUp)
downBtn.SetText(t.moveDown)
outEntry.SetPlaceHolder(t.outPlaceholder)
chooseOutBtn.SetText(t.chooseOut)
dividerCheck.Text = t.dividerCheck
dividerCheck.Refresh()
encryptCheck.Text = t.encryptCheck
encryptCheck.Refresh()
mergeBtn.SetText(t.mergeBtn)
aboutBtn.SetText(t.aboutBtn)
passwordEntry.SetPlaceHolder(t.passwordPlace)
mergeLabel.SetText(t.sectionMerge)
encryptLabel.SetText(t.sectionEnc)
langLabelPrefix.SetText(t.langLabel)
}
mergeBtn.OnTapped = func() {
t := translations[currentLang]
// ๆพ่กๆขไปถ๏ผ
// - ๅคๆช๏ผ้ >= 2
// - ๅฎๆช๏ผ่ฅๅพ้ธๅ ๅฏ๏ผๅ
่จฑ๏ผ่ฅๆชๅพ้ธๅ ๅฏ๏ผไป้ >= 2
if !((len(files) >= 2) || (encryptCheck.Checked && len(files) == 1)) {
dialog.ShowInformation("Info", t.msgErrMinFile, w)
return
}
finalOut := strings.TrimSpace(outEntry.Text)
if finalOut == "" || finalOut == ".pdf" {
dialog.ShowInformation("Info", t.msgErrNoOut, w)
return
}
if encryptCheck.Checked && strings.TrimSpace(passwordEntry.Text) == "" {
dialog.ShowInformation("Info", t.msgErrNoPW, w)
return
}
// === ๆฐๅข๏ผไบๅๆๆ็กๆณๅไฝต็ๆชๆก๏ผๅ ๅฏ/ไฟ่ญท/ไธๅฏ่ฎ๏ผ ===
filesToCheck := files
badFiles := make([]string, 0, len(filesToCheck))
for _, f := range filesToCheck {
if isEncryptedOrProtected(f) {
badFiles = append(badFiles, filepath.Base(f))
}
}
if len(badFiles) > 0 {
// ้กฏ็คบๆธ
ๅฎไธฆไธญๆญข๏ผไธ้ฒๅ
ฅ่็
msg := t.msgEncryptedListHeader + "\n"
for _, name := range badFiles {
msg += "- " + name + "\n"
}
dialog.ShowInformation("Info", msg, w)
return
}
// === ไบๅๆๆ็ตๆ ===
mergeBtn.Disable()
progressIndicator.Show()
sizeResultLabel.SetText(t.processing)
go func() {
err := runPdfProcess(files, finalOut, dividerCheck.Checked, encryptCheck.Checked, passwordEntry.Text)
var sizeStr string
if err == nil {
if fInfo, statErr := os.Stat(finalOut); statErr == nil {
sizeStr = formatFileSize(fInfo.Size())
}
}
fyne.Do(func() {
if err != nil {
dialog.ShowError(err, w)
} else {
sizeResultLabel.SetText(t.outputSizeLabel + sizeStr)
dialog.ShowInformation("OK", t.msgSuccess+"\n"+finalOut, w)
}
mergeBtn.Enable()
progressIndicator.Hide()
})
}()
}
addBtn.OnTapped = func() {
fd := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
if err != nil || r == nil {
return
}
path := cleanPath(r.URI().Path())
if strings.EqualFold(filepath.Ext(path), ".pdf") {
files = append(files, path)
list.Refresh()
}
}, w)
fd.SetFilter(storage.NewExtensionFileFilter([]string{".pdf"}))
fd.Show()
}
addFolderBtn.OnTapped = func() {
dialog.ShowFolderOpen(func(lu fyne.ListableURI, err error) {
if err != nil || lu == nil {
return
}
entries, _ := lu.List()
for _, entry := range entries {
path := cleanPath(entry.Path())
if strings.EqualFold(filepath.Ext(path), ".pdf") {
files = append(files, path)
}
}
list.Refresh()
}, w)
}
removeBtn.OnTapped = func() {
if selectedIndex < 0 || selectedIndex >= len(files) {
return
}
files = append(files[:selectedIndex], files[selectedIndex+1:]...)
selectedIndex = -1
list.Refresh()
}
upBtn.OnTapped = func() {
if selectedIndex <= 0 {
return
}
files[selectedIndex-1], files[selectedIndex] = files[selectedIndex], files[selectedIndex-1]
selectedIndex--
list.Refresh()
list.Select(selectedIndex)
}
downBtn.OnTapped = func() {
if selectedIndex < 0 || selectedIndex >= len(files)-1 {
return
}
files[selectedIndex+1], files[selectedIndex] = files[selectedIndex], files[selectedIndex+1]
selectedIndex++
list.Refresh()
list.Select(selectedIndex)
}
chooseOutBtn.OnTapped = func() {
fs := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) {
if err != nil || wc == nil {
return
}
// ไฝฟ็จ่
ๅฏฆ้ๅจ SaveDialog ้ธๅฐ็ๅๅง่ทฏๅพ๏ผๅฏ่ฝๆชๅซ .pdf๏ผ
raw := cleanPath(wc.URI().Path())
out := raw
if !strings.HasSuffix(strings.ToLower(out), ".pdf") {
out += ".pdf"
}
// ้้ Writer๏ผๅฟ
่ฆ๏ผ๏ผไฝๆญคๆญฅๅฏ่ฝๆๅจ raw ไฝ็ฝฎๅปบ็ซ้ถไฝๅ
็ตๆชๆก
_ = wc.Close()
// UI ้กฏ็คบๆ็ต่ผธๅบ่ทฏๅพ๏ผๅซ .pdf๏ผ
outEntry.SetText(out)
// ไฟฎๆญฃ๏ผๅช้ค SaveDialog ๅฏ่ฝๅปบ็ซ็็ฉบ็ฝๅ ไฝๆช๏ผ0 bytes๏ผ
removeIfEmpty(raw)
if out != raw {
removeIfEmpty(out)
}
}, w)
fs.SetFileName("merged.pdf")
fs.Show()
}
aboutBtn.OnTapped = func() {
t := translations[currentLang]
dialog.ShowInformation(t.aboutTitle, t.aboutContent, w)
}
langOptions := []string{"็น้ซไธญๆ", "English", "ๆฅๆ", "Deutsch"}
langSelect := widget.NewSelect(langOptions, func(s string) {
a.Preferences().SetString(prefLangKey, s)
refreshUI(s)
})
langSelect.Selected = savedLang
langContainer := container.NewHBox(langLabelPrefix, langSelect)
toolbar := container.NewHBox(addBtn, addFolderBtn, removeBtn, upBtn, downBtn, layout.NewSpacer(), langContainer, aboutBtn)
settings := container.NewVBox(
widget.NewSeparator(),
mergeLabel,
container.NewBorder(nil, nil, nil, chooseOutBtn, outEntry),
dividerCheck,
widget.NewSeparator(),
encryptLabel,
encryptCheck,
container.NewVBox(passwordEntry, strengthLabel),
widget.NewSeparator(),
progressIndicator,
sizeResultLabel,
mergeBtn,
)
list.OnSelected = func(id widget.ListItemID) { selectedIndex = int(id) }
refreshUI(savedLang)
w.SetContent(container.NewBorder(
toolbar, nil, nil, nil,
container.NewHSplit(list, container.NewPadded(settings)),
))
w.ShowAndRun()
}
โ ๏ธ Disclaimer
The author is not responsible for any data loss, corruption, or other consequences resulting from the use of this software.




