Skip to content

Commit bb7e7e6

Browse files
committed
first implementation
0 parents  commit bb7e7e6

File tree

6 files changed

+307
-0
lines changed

6 files changed

+307
-0
lines changed

Makefile

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
.PHONY: build test clean install all
2+
3+
# Binary name
4+
BINARY_NAME=pdf-joiner
5+
6+
# Go parameters
7+
GOCMD=go
8+
GOBUILD=$(GOCMD) build
9+
GOTEST=$(GOCMD) test
10+
GOGET=$(GOCMD) get
11+
GOMOD=$(GOCMD) mod
12+
GOCLEAN=$(GOCMD) clean
13+
14+
# Build flags
15+
LDFLAGS=-ldflags "-s -w"
16+
17+
all: test build
18+
19+
build:
20+
$(GOBUILD) $(LDFLAGS) -o $(BINARY_NAME) -v
21+
22+
test:
23+
$(GOTEST) -v ./...
24+
25+
clean:
26+
$(GOCLEAN)
27+
rm -f $(BINARY_NAME)
28+
rm -f $(BINARY_NAME).exe
29+
30+
run:
31+
$(GOBUILD) -o $(BINARY_NAME) -v
32+
./$(BINARY_NAME)
33+
34+
install: build
35+
mv $(BINARY_NAME) /usr/local/bin/
36+
37+
# Dependencies
38+
deps:
39+
$(GOMOD) tidy
40+
41+
# Cross compilation
42+
build-darwin:
43+
GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BINARY_NAME)-darwin-amd64 -v
44+
GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) -o $(BINARY_NAME)-darwin-arm64 -v
45+
46+
# Universal binary for macOS (both Intel and Apple Silicon)
47+
build-universal-darwin: build-darwin
48+
lipo -create -output $(BINARY_NAME) $(BINARY_NAME)-darwin-amd64 $(BINARY_NAME)-darwin-arm64
49+
rm $(BINARY_NAME)-darwin-amd64 $(BINARY_NAME)-darwin-arm64

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# PDF Joiner
2+
3+
A simple command-line utility for joining multiple PDF files into a single PDF document on macOS.
4+
5+
## Requirements
6+
7+
- macOS (This tool uses the built-in macOS PDF joining utility)
8+
- Go 1.18 or later (for building from source)
9+
10+
## Installation
11+
12+
### From Source
13+
14+
1. Clone this repository:
15+
```
16+
git clone https://github.com/vinitkumar/pdf-joiner.git
17+
cd pdf-joiner
18+
```
19+
20+
2. Build the binary:
21+
```
22+
make build
23+
```
24+
25+
3. (Optional) Install the binary to your system:
26+
```
27+
make install
28+
```
29+
30+
## Usage
31+
32+
```
33+
pdf-joiner [-o output.pdf] file1.pdf file2.pdf [file3.pdf ...]
34+
```
35+
36+
### Options
37+
38+
- `-o`: Specify the output file path. If not provided, the output will be saved as `joined-pdf-YYYY-MM-DD-HHMMSS.pdf` in the current directory.
39+
40+
### Examples
41+
42+
Join two PDF files:
43+
```
44+
pdf-joiner file1.pdf file2.pdf
45+
```
46+
47+
Join multiple PDF files with a specific output path:
48+
```
49+
pdf-joiner -o merged.pdf file1.pdf file2.pdf file3.pdf
50+
```
51+
52+
Join all PDF files in a directory:
53+
```
54+
pdf-joiner -o merged.pdf /path/to/directory/*.pdf
55+
```
56+
57+
## How It Works
58+
59+
This tool is a wrapper around the macOS built-in PDF joining utility located at:
60+
```
61+
/System/Library/Automator/Combine PDF Pages.action/Contents/MacOS/join
62+
```
63+
64+
## Development
65+
66+
### Running Tests
67+
68+
```
69+
make test
70+
```
71+
72+
### Building for Different Architectures
73+
74+
Build for both Intel and Apple Silicon Macs:
75+
```
76+
make build-universal-darwin
77+
```
78+
79+
### Cleaning Up
80+
81+
```
82+
make clean
83+
```
84+
85+
## License
86+
87+
MIT
88+
89+
## Author
90+
91+
Vinit Kumar

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/vinitkumar/pdf-joiner
2+
3+
go 1.24.0

main.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"time"
10+
)
11+
12+
const (
13+
// Path to the Mac PDF joiner utility
14+
pdfJoinerPath = "/System/Library/Automator/Combine PDF Pages.action/Contents/MacOS/join"
15+
)
16+
17+
func main() {
18+
// Define command-line flags
19+
outputPath := flag.String("o", "", "Output path for the joined PDF")
20+
flag.Parse()
21+
22+
// Get the PDF files to join from the remaining arguments
23+
pdfFiles := flag.Args()
24+
if len(pdfFiles) < 2 {
25+
fmt.Println("Error: At least two PDF files are required for joining")
26+
fmt.Println("Usage: pdf-joiner [-o output.pdf] file1.pdf file2.pdf [file3.pdf ...]")
27+
os.Exit(1)
28+
}
29+
30+
// Validate that all input files exist and are PDFs
31+
for _, file := range pdfFiles {
32+
if !fileExists(file) {
33+
fmt.Printf("Error: File '%s' does not exist\n", file)
34+
os.Exit(1)
35+
}
36+
37+
if filepath.Ext(file) != ".pdf" {
38+
fmt.Printf("Warning: File '%s' may not be a PDF file\n", file)
39+
}
40+
}
41+
42+
// If no output path is provided, create a default one with the current date
43+
if *outputPath == "" {
44+
currentTime := time.Now().Format("2006-01-02-150405")
45+
*outputPath = fmt.Sprintf("joined-pdf-%s.pdf", currentTime)
46+
}
47+
48+
// Ensure the output directory exists
49+
outputDir := filepath.Dir(*outputPath)
50+
if outputDir != "." && outputDir != "" {
51+
if err := os.MkdirAll(outputDir, 0755); err != nil {
52+
fmt.Printf("Error creating output directory: %v\n", err)
53+
os.Exit(1)
54+
}
55+
}
56+
57+
// Check if the Mac PDF joiner utility exists
58+
if !fileExists(pdfJoinerPath) {
59+
fmt.Printf("Error: PDF joiner utility not found at '%s'\n", pdfJoinerPath)
60+
fmt.Println("This tool only works on macOS systems.")
61+
os.Exit(1)
62+
}
63+
64+
// Prepare the command to join PDFs
65+
args := []string{"-o", *outputPath}
66+
args = append(args, pdfFiles...)
67+
68+
// Execute the command
69+
cmd := exec.Command(pdfJoinerPath, args...)
70+
output, err := cmd.CombinedOutput()
71+
72+
if err != nil {
73+
fmt.Printf("Error joining PDFs: %v\n", err)
74+
fmt.Printf("Command output: %s\n", output)
75+
os.Exit(1)
76+
}
77+
78+
fmt.Printf("Successfully joined %d PDF files into '%s'\n", len(pdfFiles), *outputPath)
79+
}
80+
81+
// fileExists checks if a file exists and is not a directory
82+
func fileExists(path string) bool {
83+
info, err := os.Stat(path)
84+
if os.IsNotExist(err) {
85+
return false
86+
}
87+
return !info.IsDir()
88+
}

main_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestFileExists(t *testing.T) {
10+
// Create a temporary file for testing
11+
tempFile, err := os.CreateTemp("", "test-*.txt")
12+
if err != nil {
13+
t.Fatalf("Failed to create temporary file: %v", err)
14+
}
15+
defer os.Remove(tempFile.Name())
16+
defer tempFile.Close()
17+
18+
// Test cases
19+
tests := []struct {
20+
name string
21+
path string
22+
expected bool
23+
}{
24+
{
25+
name: "Existing file",
26+
path: tempFile.Name(),
27+
expected: true,
28+
},
29+
{
30+
name: "Non-existing file",
31+
path: "non-existing-file.pdf",
32+
expected: false,
33+
},
34+
{
35+
name: "Directory",
36+
path: ".",
37+
expected: false, // fileExists returns false for directories
38+
},
39+
}
40+
41+
// Run test cases
42+
for _, tc := range tests {
43+
t.Run(tc.name, func(t *testing.T) {
44+
result := fileExists(tc.path)
45+
if result != tc.expected {
46+
t.Errorf("fileExists(%s) = %v, expected %v", tc.path, result, tc.expected)
47+
}
48+
})
49+
}
50+
}
51+
52+
func TestOutputPathGeneration(t *testing.T) {
53+
// This is a mock test to demonstrate how we would test the output path generation
54+
// In a real test, we would need to mock time.Now() or use dependency injection
55+
56+
// Create a temporary directory for testing
57+
tempDir, err := os.MkdirTemp("", "pdf-joiner-test")
58+
if err != nil {
59+
t.Fatalf("Failed to create temporary directory: %v", err)
60+
}
61+
defer os.RemoveAll(tempDir)
62+
63+
// Test that we can create directories for output
64+
testOutputPath := filepath.Join(tempDir, "subdir", "output.pdf")
65+
outputDir := filepath.Dir(testOutputPath)
66+
67+
err = os.MkdirAll(outputDir, 0755)
68+
if err != nil {
69+
t.Errorf("Failed to create output directory: %v", err)
70+
}
71+
72+
// Verify the directory was created
73+
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
74+
t.Errorf("Output directory was not created: %v", err)
75+
}
76+
}

pdf-joiner

1.71 MB
Binary file not shown.

0 commit comments

Comments
 (0)