Adult woman typing on a laptop at a desk with a smartphone and documents.

[Sharing] PDF Merge and Encryption Tool V1.0 (Offline Version)

A fully offline PDF merge and encryption tool for Windows, written in Go. Merge multiple PDFs, encrypt with AES-256, and keep your documents secure without uploading files online.

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.


Leave a Reply

Your email address will not be published. Required fields are marked *