Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]

jobs:
test-and-build:
name: Build & Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'

- name: Run Tests
run: go test -v ./...

- name: Build Binary
run: go build -v ./...
32 changes: 32 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Release

on:
push:
tags:
- 'v*'

permissions:
contents: write

jobs:
goreleaser:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
13 changes: 5 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
gophersnap
gophersnap.exe
*.exe
*.exe~
*.dll
*.so
*.dylib
*.zip
*.tar.gz

# Test binary, built with `go test -c`
*.test
Expand All @@ -17,7 +18,7 @@ coverage.*
*.coverprofile
profile.cov

# Dependency directories (remove the comment below to include it)
# Dependency directories
# vendor/

# Go workspace file
Expand All @@ -26,7 +27,3 @@ go.work.sum

# env file
.env

# Editor/IDE
# .idea/
# .vscode/
52 changes: 52 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
version: 2

before:
hooks:
- go mod tidy

builds:
- id: gophersnap
main: ./main.go
binary: gophersnap
env:
- CGO_ENABLED=0
goos:
- windows
- linux
- darwin
goarch:
- amd64
- arm64
ldflags:
- -s -w -X github.com/benjaminjvdm/GopherSnap/cmd.Version={{.Version}} -X github.com/benjaminjvdm/GopherSnap/cmd.Commit={{.Commit}} -X github.com/benjaminjvdm/GopherSnap/cmd.Date={{.Date}}

universal_binaries:
- id: gophersnap
replace: true

archives:
- id: default
formats:
- tar.gz
format_overrides:
- goos: windows
formats:
- zip
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
files:
- README.md
- LICENSE

checksum:
name_template: "checksums.txt"
algorithm: sha256

snapshot:
version_template: "{{ incpatch .Version }}-next"

changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
20 changes: 20 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.PHONY: all build test clean release

BINARY_NAME=gophersnap
WINDOWS_BINARY=gophersnap.exe

all: build

build:
CGO_ENABLED=0 go build -ldflags="-s -w" -o $(BINARY_NAME) main.go

test:
go test -v -cover ./...

clean:
rm -f $(BINARY_NAME) $(WINDOWS_BINARY) gophersnap_*.zip gophersnap_*.tar.gz coverage.out

release:
go run github.com/tc-hib/go-winres@latest make --arch amd64,arm64,386
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o $(WINDOWS_BINARY) main.go
zip -q gophersnap_windows_x64.zip $(WINDOWS_BINARY) README.md LICENSE
107 changes: 102 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,92 @@
# GopherSnap

GopherSnap is a high-performance, concurrent CLI image converter written in Go. It allows you to batch process images with efficiency, supporting modern formats like WebP and AVIF.
GopherSnap is a high-performance, concurrent CLI image converter written in Go. It allows you to batch process images with efficiency, supporting modern formats like WebP and AVIF with zero external runtime C dependencies.

## Features

- **Batch Conversion**: Process entire directories of images at once.
- **Recursive Directory Mirroring**: Preserves input directory structure in the output.
- **Concurrent Processing**: Leverages Go's goroutines for fast, parallel execution.
- **Modern Formats**: Supports JPG, PNG, WebP, and AVIF.
- **No CGO**: Uses pure-Go/WASM implementations for WebP and AVIF for easy portability.
- **Pure-Go & Zero CGO**: Compiles cleanly with `CGO_ENABLED=0` using WASM/pure-Go encoders for maximum cross-platform portability.
- **Standalone Windows Executable**: Embedded application icons and PE version manifests for Windows (`.exe`).
- **Interactive Progress**: Real-time feedback with a styled progress bar.

## Architecture Overview

GopherSnap is architected for zero-CGO portability and parallel performance:

- **Core Converter Pipeline**: Pure Go image decoding (`image/jpeg`, `image/png`, `image/gif`) combined with pure-Go and WASM-backed WebP (`github.com/gen2brain/webp`) and AVIF (`github.com/gen2brain/avif`) encoders via Wazero.
- **Concurrency Model**: Worker pool pattern (`internal/converter/batch.go`) distributing file transformation jobs across customizable goroutine workers.
- **Windows Embedding**: Windows application icon and PE metadata are compiled into native `rsrc_windows_*.syso` resources using `go-winres`.
- **Cross-Platform Utility**: Clean path sanitization and case-insensitive image extension matching (`.jpg`, `.jpeg`, `.png`, `.webp`, `.avif`, `.gif`).

## Installation

Ensure you have Go installed on your system. Then, you can install GopherSnap using:
### Windows (.EXE) Standalone Quickstart

Download `gophersnap_windows_x64.zip` from the latest GitHub Release, extract `gophersnap.exe`, and run it directly in PowerShell or Command Prompt:

```powershell
# Extract gophersnap.exe and test version
.\gophersnap.exe version

# Convert images in PowerShell
.\gophersnap.exe convert -i C:\Images\Photos -o C:\Images\Output -f webp -q 85
```

### Via Go Install

Ensure Go is installed on your system:

```bash
go install github.com/benjaminjvdm/GopherSnap@latest
```

Alternatively, clone the repository and build it manually:
### Build From Source

Clone the repository and build using Makefile or PowerShell build automation:

```bash
git clone https://github.com/benjaminjvdm/GopherSnap.git
cd GopherSnap
go build -o gophersnap

# Linux / macOS (Unix)
make build

# Windows PowerShell
.\build.ps1
```

## Build Automation & PowerShell Scripts

### Windows PowerShell (`build.ps1`)

```powershell
# Default build: compiles standalone gophersnap.exe with stripped symbols
.\build.ps1

# Run full test suite with coverage report
.\build.ps1 -Test

# Generate Windows PE resources and package release ZIP
.\build.ps1 -Package
```

### Unix Makefile (`Makefile`)

```bash
# Build standalone binary
make build

# Run test suite
make test

# Generate Windows PE resources and release package
make release

# Clean binaries and build artifacts
make clean
```

## Usage
Expand Down Expand Up @@ -70,3 +132,38 @@ gophersnap convert -i ./images --width 1024 --height 1024
- `--height int`: Target height (maintaining aspect ratio)
- `-j, --jobs int`: Number of concurrent jobs (Default: `4`)
- `--overwrite`: Overwrite existing files if they exist in the output directory

## Version & Release Pipeline

GopherSnap features automated cross-platform releases powered by GoReleaser and GitHub Actions.

### Version Command & Linker Injection

Build-time metadata can be injected via ldflags:

```bash
go build -ldflags="-s -w \
-X 'github.com/benjaminjvdm/GopherSnap/cmd.Version=v0.1.0' \
-X 'github.com/benjaminjvdm/GopherSnap/cmd.Commit=1a2b3c4' \
-X 'github.com/benjaminjvdm/GopherSnap/cmd.Date=2026-03-30'" \
-o gophersnap main.go
```

Check installed version details:

```bash
gophersnap version
# or
gophersnap --version
```

### Release Process & GoReleaser Configuration

To trigger an automated release:
1. Commit all changes and create a semantic version tag: `git tag -a v0.1.0 -m "Release v0.1.0"`.
2. Push the tag: `git push origin v0.1.0`.
3. GitHub Actions (`.github/workflows/release.yml`) executes `.goreleaser.yaml` to build cross-platform binaries:
- **Windows**: `windows/amd64`, `windows/arm64` (.zip with `gophersnap.exe`, `README.md`, `LICENSE`)
- **Linux**: `linux/amd64`, `linux/arm64` (.tar.gz)
- **macOS**: `darwin/amd64`, `darwin/arm64` (Universal binary .tar.gz)
- **Checksums**: SHA-256 (`checksums.txt`)
37 changes: 37 additions & 0 deletions build.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[CmdletBinding()]
param(
[switch]$Test,
[switch]$Package
)

$ErrorActionPreference = "Stop"

if ($Test) {
Write-Host "Running full test suite..." -ForegroundColor Cyan
go test -v -cover ./...
exit $LASTEXITCODE
}

if ($Package) {
Write-Host "Generating Windows PE resources..." -ForegroundColor Cyan
go run github.com/tc-hib/go-winres@latest make --arch amd64,arm64,386

Write-Host "Building gophersnap.exe..." -ForegroundColor Cyan
$env:CGO_ENABLED = "0"
$env:GOOS = "windows"
$env:GOARCH = "amd64"
go build -ldflags="-s -w" -o gophersnap.exe main.go

Write-Host "Packaging gophersnap_windows_x64.zip..." -ForegroundColor Cyan
if (Test-Path "gophersnap_windows_x64.zip") {
Remove-Item "gophersnap_windows_x64.zip" -Force
}
Compress-Archive -Path "gophersnap.exe", "README.md", "LICENSE" -DestinationPath "gophersnap_windows_x64.zip"
Write-Host "Successfully packaged gophersnap_windows_x64.zip" -ForegroundColor Green
exit 0
}

Write-Host "Building standalone gophersnap.exe..." -ForegroundColor Cyan
$env:CGO_ENABLED = "0"
go build -ldflags="-s -w" -o gophersnap.exe main.go
Write-Host "Build complete: gophersnap.exe" -ForegroundColor Green
4 changes: 2 additions & 2 deletions cmd/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ var convertCmd = &cobra.Command{
if err != nil {
relPath = "."
}
jobOutputDir := filepath.Join(outputDir, relPath)
jobOutputDir := filepath.Clean(filepath.Join(outputDir, relPath))
jobsList = append(jobsList, converter.ConverterJob{
InputPath: file,
OutputDir: jobOutputDir,
Expand Down Expand Up @@ -176,7 +176,7 @@ func findFiles(path string) ([]string, error) {
if !info.IsDir() {
ext := strings.ToLower(filepath.Ext(p))
if ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".webp" || ext == ".avif" || ext == ".gif" {
files = append(files, p)
files = append(files, filepath.Clean(p))
}
}
return nil
Expand Down
Loading
Loading