diff --git a/.gitignore b/.gitignore index ddb5241..3bdd893 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ go-app/_pdfs pdfminion docs/website/_site docs/_site +go-app/build diff --git a/README.md b/README.md index 452d7a5..5e817d0 100644 --- a/README.md +++ b/README.md @@ -7,15 +7,12 @@ Helper (_minion_) for some mundane tasks with PDF documents, among others: * add header and/or footer text * concatenate (combine multiple PDF files into a single file) -It shall have a (multi-platform) graphical user interface, at least for Mac-OS, Windows and maybe Linux. > minion: a servile dependent, follower, or underling.
> "He's one of the boss's minions."
> From: [Merriam-Webster Dictionary](https://www.merriam-webster.com/dictionary/minion) ## Status -[![feature-linter](https://github.com/gernotstarke/PDFminion/actions/workflows/feature-linter.yml/badge.svg)](https://github.com/gernotstarke/PDFminion/actions/workflows/feature-linter.yml) -[![go_test](https://github.com/gernotstarke/PDFminion/actions/workflows/go_test.yml/badge.svg)](https://github.com/gernotstarke/PDFminion/actions/workflows/go_test.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/gernotstarke/PDFminion)](https://goreportcard.com/report/github.com/gernotstarke/PDFminion) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=gernotstarke_PDFminion&metric=alert_status)](https://sonarcloud.io/dashboard?id=gernotstarke_PDFminion) @@ -23,74 +20,8 @@ It shall have a (multi-platform) graphical user interface, at least for Mac-OS, [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) I'm currently experimenting with an MVP version of PDFminion, therefore most of the information given here is **NOT** valid any longer. -## Why PDFminion? +## Why PDFminion? -## Development - -#### The following paragraphs are outdated!! - -We're using BDD (behavior driven development) with Cucumber to specify at least part of the requirements as _scenarios_. -These scenarios can be executed, similar to automated unit tests. - -* [Godog](https://github.com/cucumber/godog), the official Cucumber tool -* [Cucumber HTML Reporter](https://www.npmjs.com/package/cucumber-html-reporter) -* [Cucumber Multi Reporter (more detailed)](https://github.com/wswebcreation/multiple-cucumber-html-reporter) - -Use `./create-detailed-cucumber-report.sh` to generate a detailed BDD Cucumber report. - -### Deviation from Standard golang practices -As of June 2021, the `godog` bdd tool does not respect the standard golang layout practice -of putting test files next to the tested-code. -Instead, the step definitions need to be present in the root folder! - -To avoid confusion, I prefixed the step definitions with `stepdef_` - so they are easily recognizable. -Other (non-bdd/cucumber) automated tests will reside within the appropriate package folders. - -See this [Cucumber/godog issue](https://github.com/cucumber/godog/issues/373). - -### Godog - -````shell -go get github.com/cucumber/godog/cmd/godog@v0.12.0 -```` -### Cucumber HTML Reporter - -It's written in JavaScript and requires `npm` and `node` to be available on your machine. - -```shell -npm install cucumber-html-reporter --save-dev -``` - -### Cucumber Multi Reporter - -Again, JavaScript, see above: - -```shell -npm install multiple-cucumber-html-reporter --save-dev -``` - - -## Usage of Development Tools - -I squeezed the required commands into the files `create-cucumber-report.sh` -and `create-detailed-cucumber-report.sh`. - -### Godog - -```shell -godog --format cucumber:test-results-results/cucumber-report.json -``` - -Notes: - -* godog requires features and scenarios to be written in a `features` directory. -* the `--format` switch can take a file and/or directory name - - -### Cucumber Report - -```shell -node ./assets/simple-cucumber-report.js -``` - +When creating PDF documents, you often need to add page numbers, headers, footers, or combine multiple PDF files into a single file. +PDFminion can do that for you - from a terminal and the command line, on MacOS, Linux and Windows platforms. diff --git a/changelog.md b/changelog.md index 50c5208..aa94123 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,5 @@ ## PDFminion version history - +0.3.1 Nov 16th 2024: refactored repository layout, fixed wrong output 0.3.0 better structure, minimal main package, moved logic to process.go 0.2.5 fixed nasty bug in numbering 0.2.3 add --force flag to allow existing target directory diff --git a/documentation/adr/.adr-dir b/documentation/adr/.adr-dir new file mode 100644 index 0000000..9c558e3 --- /dev/null +++ b/documentation/adr/.adr-dir @@ -0,0 +1 @@ +. diff --git a/documentation/adr/0001-use-adrs.md b/documentation/adr/0001-use-adrs.md new file mode 100644 index 0000000..d8d4924 --- /dev/null +++ b/documentation/adr/0001-use-adrs.md @@ -0,0 +1,46 @@ +# 1. Use-ADRs +# Architecture Decision Record: Using ADRs for Technical Documentation + +## Status +Accepted + +## Date: 2024-11-18 + +## Context +We need a sustainable way to document technical decisions that: +- Captures the context and reasoning at the time of decision +- Is easy to maintain alongside code +- Provides historical context for future maintainers +- Supports clear communication within the team + +## Decision +We will use Architecture Decision Records (ADRs) as our primary means of documenting significant technical decisions. + +## Format +Each ADR will be: +- Written in Markdown +- Stored in `/documentation/adr` directory +- Named using pattern: `NNNN-title-with-dashes.md` +- Include sections: Status, Date, Context, Decision, Consequences + +## Reasons + +1. **Time-Stamped Context** +- Captures why decisions were made at a specific point in time +- Helps future maintainers understand historical choices + +2. **Version Control Integration** +- ADRs live with the code +- Changes tracked in git + + +### Cons +1. **Maintenance Required** +- Must be kept up to date +- Requires discipline to create consistently + + +## Notes +- ADRs are immutable once accepted +- Superseded decisions should be marked as such +- Not every decision needs an ADR - focus on significant architectural choices \ No newline at end of file diff --git a/documentation/adr/0002-use-make-as-build-tool.md b/documentation/adr/0002-use-make-as-build-tool.md new file mode 100644 index 0000000..7f40338 --- /dev/null +++ b/documentation/adr/0002-use-make-as-build-tool.md @@ -0,0 +1,107 @@ +# 2. Use-make-as-build-tool + +## Status +Accepted + +## Date +2024-11-18 + +## Context +We needed to choose a build tool for our Go application PDFminion that would handle: +- Cross-platform compilation +- Multiple build targets +- Release packaging +- Installation/uninstallation +- Development workflows + +The main alternatives considered were: +- Make +- Shell scripts +- Go's built-in build commands + +## Decision +We decided to use Make as our primary build tool. + +## Reasons + +### Pros +1. **Ubiquity** +- Make is installed by default on most Unix-like systems +- Well-understood by most developers +- Extensive documentation available +- Long history of reliability + +2. **Platform Independence** +- Works on all major platforms (Linux, macOS, Windows via WSL) +- Consistent behavior across environments + +3. **Dependency Management** +- Built-in dependency tracking +- Efficient rebuilds (only rebuilds what's necessary) +- Clear visualization of build dependencies + +4. **Simplicity** +- Declarative syntax +- No additional dependencies needed +- Easy to maintain and modify +- Self-documenting through target names + +5. **Flexibility** +- Can execute any shell command +- Easy to add new targets +- Supports both simple and complex build processes +- Can integrate with other tools seamlessly + + +### Cons +1. **Windows Compatibility** +- Requires WSL or MinGW on Windows +- May create friction for Windows developers + +2. **Syntax** +- Tab-based syntax can be error-prone +- Learning curve for complex features +- Limited string manipulation capabilities + +3. **Error Handling** +- Basic error handling capabilities +- Can be verbose for complex error scenarios + +4. **Debugging** +- Limited built-in debugging facilities +- Can be hard to troubleshoot complex makefiles + +## Consequences + +### Positive +1. **Development Workflow** +- Simple commands like `make mac` or `make release` +- Easy to remember and use +- Quick to execute +- Consistent across team members + +2. **Maintenance** +- Single file (Makefile) contains all build logic +- Easy to add new targets and modify existing ones +- Version control friendly +- Self-documenting + +3. **Integration** +- Easy integration with CI/CD pipelines +- Works well with existing Go tools +- Can be extended with shell scripts if needed + +### Negative +1. **Team Requirements** +- Team members need basic Make knowledge +- Windows developers need additional setup +- May need documentation for complex targets + +2. **Scaling** +- Complex build processes may become hard to maintain +- Limited modularity compared to modern build tools +- May need to supplement with scripts for complex tasks + +## Notes +- Maintain clear documentation of available make targets + diff --git a/go-app/Makefile b/go-app/Makefile index a02bd74..0dad534 100644 --- a/go-app/Makefile +++ b/go-app/Makefile @@ -5,52 +5,177 @@ GOCMD=go GOBUILD=$(GOCMD) build GOCLEAN=$(GOCMD) clean GOTEST=$(GOCMD) test -GOGET=$(GOCMD) get BINARY_NAME=pdfminion -BINARY_UNIX=$(BINARY_NAME)_unix +VERSION=0.3.1 + +# Build directories +BUILD_DIR=build +DIST_DIR=dist # Build information BUILDTIME=$(shell date -u +'%Y %b %d %H:%M') +COMMIT=$(shell git rev-parse --short HEAD) + +# Detect host platform +UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) + +# Convert to normalized platform string +ifeq ($(UNAME_S),Darwin) + HOST_OS := MacOS +else ifeq ($(UNAME_S),Linux) + HOST_OS := linux +else + HOST_OS := unknown +endif -# Build flags -LDFLAGS=-ldflags "-s -w -X 'pdfminion/internal/config.BuildTime=$(BUILDTIME)'" +ifeq ($(UNAME_M),x86_64) + HOST_ARCH := amd64 +else ifeq ($(UNAME_M),arm64) + HOST_ARCH := arm64 +else + HOST_ARCH := unknown +endif -# Install directory +HOST_PLATFORM := $(HOST_OS)-$(HOST_ARCH) + +# Add to LDFLAGS +LDFLAGS=-ldflags "-s -w \ + -X 'pdfminion/internal/cli.buildTime=$(BUILDTIME)' \ + -X 'pdfminion/internal/cli.hostPlatform=$(HOST_PLATFORM)'" +# Install directory (for Unix-like systems) INSTALL_DIR=/usr/local/bin -.PHONY: all build clean test run install uninstall +# Platform specific settings +WINDOWS_AMD64=windows-amd64 +LINUX_AMD64=linux-amd64 +DARWIN_AMD64=darwin-amd64 +DARWIN_ARM64=darwin-arm64 + +.PHONY: all build clean test run install uninstall release \ + build-windows-amd64 build-linux-amd64 build-darwin-amd64 build-darwin-arm64 \ + package-windows-amd64 package-linux-amd64 package-darwin-amd64 package-darwin-arm64 \ + mac + +# Directory creation +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) -all: test build +$(DIST_DIR): + mkdir -p $(DIST_DIR) -build: +# Standard development build (current platform) +build: $(BUILD_DIR) + @echo "Building for current platform..." @echo "Build Time: $(BUILDTIME)" - @echo "LDFLAGS: $(LDFLAGS)" - $(GOBUILD) $(LDFLAGS) -o $(BINARY_NAME) -v ./cmd/pdfminion + $(GOBUILD) $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME) -v ./cmd/pdfminion -test: - $(GOTEST) -v ./... +# Shortcut for Apple Silicon build +mac: build-darwin-arm64 + @echo "Apple Silicon build available in $(BUILD_DIR)/$(BINARY_NAME)-$(DARWIN_ARM64)/$(BINARY_NAME)" + +# Platform specific builds +build-windows-amd64: $(BUILD_DIR) + @echo "Building for Windows (amd64)..." + GOOS=windows GOARCH=amd64 $(GOBUILD) $(LDFLAGS) \ + -o $(BUILD_DIR)/$(BINARY_NAME)-$(WINDOWS_AMD64)/$(BINARY_NAME).exe \ + -v ./cmd/pdfminion + +build-linux-amd64: $(BUILD_DIR) + @echo "Building for Linux (amd64)..." + GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) \ + -o $(BUILD_DIR)/$(BINARY_NAME)-$(LINUX_AMD64)/$(BINARY_NAME) \ + -v ./cmd/pdfminion + +build-darwin-amd64: $(BUILD_DIR) + @echo "Building for macOS (amd64)..." + GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) \ + -o $(BUILD_DIR)/$(BINARY_NAME)-$(DARWIN_AMD64)/$(BINARY_NAME) \ + -v ./cmd/pdfminion +build-darwin-arm64: $(BUILD_DIR) + @echo "Building for macOS (Apple Silicon)..." + GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) \ + -o $(BUILD_DIR)/$(BINARY_NAME)-$(DARWIN_ARM64)/$(BINARY_NAME) \ + -v ./cmd/pdfminion + +# Platform specific packaging +package-windows-amd64: build-windows-amd64 $(DIST_DIR) + @echo "Packaging Windows (amd64) build..." + cd $(BUILD_DIR) && \ + zip -r ../$(DIST_DIR)/$(BINARY_NAME)-$(WINDOWS_AMD64)-$(VERSION).zip \ + $(BINARY_NAME)-$(WINDOWS_AMD64) + @echo "Windows package created in $(DIST_DIR)" + +package-linux-amd64: build-linux-amd64 $(DIST_DIR) + @echo "Packaging Linux (amd64) build..." + cd $(BUILD_DIR) && \ + tar czf ../$(DIST_DIR)/$(BINARY_NAME)-$(LINUX_AMD64)-$(VERSION).tar.gz \ + $(BINARY_NAME)-$(LINUX_AMD64) + @echo "Linux package created in $(DIST_DIR)" + +package-darwin-amd64: build-darwin-amd64 $(DIST_DIR) + @echo "Packaging macOS (amd64) build..." + cd $(BUILD_DIR) && \ + tar czf ../$(DIST_DIR)/$(BINARY_NAME)-$(DARWIN_AMD64)-$(VERSION).tar.gz \ + $(BINARY_NAME)-$(DARWIN_AMD64) + @echo "macOS package created in $(DIST_DIR)" + +package-darwin-arm64: build-darwin-arm64 $(DIST_DIR) + @echo "Packaging macOS (Apple Silicon) build..." + cd $(BUILD_DIR) && \ + tar czf ../$(DIST_DIR)/$(BINARY_NAME)-$(DARWIN_ARM64)-$(VERSION).tar.gz \ + $(BINARY_NAME)-$(DARWIN_ARM64) + @echo "macOS (Apple Silicon) package created in $(DIST_DIR)" + +# Build all platforms +release: package-windows-amd64 package-linux-amd64 package-darwin-amd64 package-darwin-arm64 + @echo "All platform builds completed!" + @ls -l $(DIST_DIR) + +# Build darwin universal binary +build-darwin-universal: $(BUILD_DIR) + @echo "Building Universal macOS binary..." + GOOS=darwin GOARCH=amd64 $(GOBUILD) $(LDFLAGS) \ + -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64/$(BINARY_NAME) \ + -v ./cmd/pdfminion + GOOS=darwin GOARCH=arm64 $(GOBUILD) $(LDFLAGS) \ + -o $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64/$(BINARY_NAME) \ + -v ./cmd/pdfminion + lipo -create \ + $(BUILD_DIR)/$(BINARY_NAME)-darwin-amd64/$(BINARY_NAME) \ + $(BUILD_DIR)/$(BINARY_NAME)-darwin-arm64/$(BINARY_NAME) \ + -output $(BUILD_DIR)/$(BINARY_NAME)-darwin-universal/$(BINARY_NAME) + +package-darwin-universal: build-darwin-universal $(DIST_DIR) + @echo "Packaging Universal macOS build..." + cd $(BUILD_DIR) && \ + tar czf ../$(DIST_DIR)/$(BINARY_NAME)-darwin-universal-$(VERSION).tar.gz \ + $(BINARY_NAME)-darwin-universal + @echo "macOS Universal package created in $(DIST_DIR)" + +# Clean build artifacts clean: $(GOCLEAN) - rm -f $(BINARY_NAME) - rm -f $(BINARY_UNIX) + rm -rf $(BUILD_DIR) + rm -rf $(DIST_DIR) -run: - $(GOBUILD) $(LDFLAGS) -o $(BINARY_NAME) -v ./cmd/pdfminion - ./$(BINARY_NAME) +# Run tests +test: + $(GOTEST) -v ./... -# Cross compilation -build-linux: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) $(LDFLAGS) -o $(BINARY_UNIX) -v ./cmd/pdfminion +# Run development build +run: build + ./$(BUILD_DIR)/$(BINARY_NAME) -# Install the binary +# Install (Unix-like systems only) install: build @echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)" - @sudo mv $(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME) + @sudo mv $(BUILD_DIR)/$(BINARY_NAME) $(INSTALL_DIR)/$(BINARY_NAME) @echo "Installation complete. You can now run '$(BINARY_NAME)' from anywhere." -# Uninstall the binary +# Uninstall uninstall: @echo "Uninstalling $(BINARY_NAME) from $(INSTALL_DIR)" @sudo rm -f $(INSTALL_DIR)/$(BINARY_NAME) - @echo "Uninstallation complete. $(BINARY_NAME) has been removed." \ No newline at end of file + @echo "Uninstallation complete." \ No newline at end of file diff --git a/go-app/go.mod b/go-app/go.mod index ea66308..e35a96b 100644 --- a/go-app/go.mod +++ b/go-app/go.mod @@ -13,10 +13,14 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 // indirect github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.3 // indirect + github.com/rs/zerolog v1.33.0 // indirect golang.org/x/image v0.5.0 // indirect + golang.org/x/sys v0.12.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go-app/go.sum b/go-app/go.sum index e0d3ec8..1b04925 100644 --- a/go-app/go.sum +++ b/go-app/go.sum @@ -1,11 +1,18 @@ +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/hhrutter/lzw v0.0.0-20190827003112-58b82c5a41cc/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650 h1:1yY/RQWNSBjJe2GDCIYoLmpWVidrooriUr4QS/zaATQ= github.com/hhrutter/lzw v0.0.0-20190829144645-6f07a24e8650/go.mod h1:yJBvOcu1wLQ9q9XZmfiPfur+3dQJuIhYQsMGLYcItZk= github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7 h1:o1wMw7uTNyA58IlEdDpxIrtFHTgnvYzA8sCQz8luv94= github.com/hhrutter/tiff v0.0.0-20190829141212-736cae8d0bc7/go.mod h1:WkUxfS2JUu3qPo6tRld7ISb8HiC0gVSU91kooBMDVok= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/pdfcpu/pdfcpu v0.4.0 h1:381iGNvMeLP+GFqIAqgd0LSj36AsK3JH4UTaF6D5jRc= @@ -17,6 +24,9 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -41,6 +51,10 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/go-app/internal/cli/cli.go b/go-app/internal/cli/cli.go new file mode 100644 index 0000000..1743748 --- /dev/null +++ b/go-app/internal/cli/cli.go @@ -0,0 +1,176 @@ +package cli + +import ( + "flag" + "fmt" + "io/ioutil" + "os" + "strings" + "time" +) + +type Options struct { + SourceDir string + TargetDir string + Force bool +} + +const ( + defaultSourceDir = "_pdfs" + defaultTargetDir = "_target" + PageNrPrefix = "" + ChapterPrefix = "Kap." + ChapterPageSeparator = " - " +) + +// Version information - injected at build time +var ( + buildTime string + hostPlatform string + appVersion string +) + +func SetAppVersion(version string) { + appVersion = version +} + +func ParseOptions() (*Options, error) { + opts := &Options{ + SourceDir: defaultSourceDir, + TargetDir: defaultTargetDir, + } + + // Define flags once + flag.StringVar(&opts.SourceDir, "source", defaultSourceDir, "Specify the source directory") + flag.StringVar(&opts.TargetDir, "target", defaultTargetDir, "Specify the target directory") + flag.BoolVar(&opts.Force, "force", false, "Forces overwrite of existing files") + + version := flag.Bool("version", false, "Show version information") + help := flag.Bool("help", false, "Show help information") + + // Add short aliases + flag.StringVar(&opts.SourceDir, "s", defaultSourceDir, "") + flag.StringVar(&opts.TargetDir, "t", defaultTargetDir, "") + flag.BoolVar(help, "h", false, "") + flag.BoolVar(version, "v", false, "") + + flag.Usage = printHelp + flag.Parse() + + // Handle help/version first + switch { + case *help: + printHelp() + os.Exit(0) + case *version: + printVersion() + os.Exit(0) + } + + // Validate the options + if err := opts.validate(); err != nil { + return nil, err + } + + return opts, nil +} + +func printHelp() { + fmt.Printf(`PDFMinion adds page numbers to existing PDF files. +It will take all PDF files from the source directory and put the numbered copies into the target directory. +Furthermore, it will ensure that every chapter (aka file) starts with an odd number +by adding a single blank page to files with an un-even page count. +When printed double-sided, every chapter will start on a right side with an odd pagenumber. + +Usage: + -s, --source string + Specify the source directory (default "%s") + -t, --target string + Specify the target directory (default "%s") + --force + Forces overwrite of existing files + -h, --help + Show this help message + -v, --version + Show version information +`, defaultSourceDir, defaultTargetDir) +} + +func printVersion() { + fmt.Printf("PDFminion version %s\n", appVersion) + fmt.Printf("Built on: %s\n", hostPlatform) + if buildTime != "" { + t, err := time.Parse("2006 Jan 02 15:04", buildTime) + if err == nil { + formattedTime := formatBuildTime(t) + fmt.Printf("Build time: %s\n", formattedTime) + } else { + fmt.Printf("Build time: %s\n", buildTime) + } + } else { + fmt.Println("Build time: Not available") + } +} + +func formatBuildTime(t time.Time) string { + formatted := t.Format("2006 Jan 02 15:04") + parts := strings.Split(formatted, " ") + if len(parts) == 4 { + day := parts[2] + parts[2] = day + getDaySuffix(day) + parts[3] += "h" + return strings.Join(parts, " ") + } + return formatted +} + +func getDaySuffix(day string) string { + switch day { + case "01", "21", "31": + return "st" + case "02", "22": + return "nd" + case "03", "23": + return "rd" + default: + return "th" + } +} + +func (o *Options) validate() error { + if err := o.validateSourceDir(); err != nil { + return err + } + return o.validateTargetDir() +} +func (o *Options) validateSourceDir() error { + if _, err := os.Stat(o.SourceDir); os.IsNotExist(err) { + return fmt.Errorf("Source directory '%s' does not exist", o.SourceDir) + } + return nil +} + +func (o *Options) validateTargetDir() error { + if _, err := os.Stat(o.TargetDir); os.IsNotExist(err) { + fmt.Printf("Target directory '%s' does not exist. Creating it...\n", o.TargetDir) + if err := os.MkdirAll(o.TargetDir, os.ModePerm); err != nil { + return fmt.Errorf("Failed to create directory '%s': %v", o.TargetDir, err) + } + return nil + } + + if o.Force { + return nil + } + + files, err := ioutil.ReadDir(o.TargetDir) + if err != nil { + return fmt.Errorf("Cannot read directory '%s': %v", o.TargetDir, err) + } + + if len(files) > 0 { + return fmt.Errorf("Target directory '%s' is not empty. Use --force to override", o.TargetDir) + } + + return nil +} diff --git a/go-app/internal/config/config.go b/go-app/internal/config/config.go deleted file mode 100644 index 43d1851..0000000 --- a/go-app/internal/config/config.go +++ /dev/null @@ -1,181 +0,0 @@ -package config - -import ( - "flag" - "fmt" - "io/ioutil" - "os" - "strings" - "time" -) - -type Config struct { - SourceDir string - TargetDir string - showHelp bool - showVersion bool - Force bool -} - -const ( - defaultSourceDir = "_pdfs" - defaultTargetDir = "_target" - Version = "0.3.0" -) - -const PageNrPrefix = "" -const ChapterPrefix = "Kap." -const ChapterPageSeparator = " - " - -// BuildTime will be injected at build time -var BuildTime string - -func New() *Config { - return &Config{ - SourceDir: defaultSourceDir, - TargetDir: defaultTargetDir, - Force: false, - } -} - -func (c *Config) ParseFlags() { - flag.StringVar(&c.SourceDir, "s", defaultSourceDir, "Specify the source directory") - flag.StringVar(&c.SourceDir, "source", defaultSourceDir, "Specify the source directory") - flag.StringVar(&c.TargetDir, "t", defaultTargetDir, "Specify the target directory") - flag.StringVar(&c.TargetDir, "target", defaultTargetDir, "Specify the target directory") - flag.BoolVar(&c.Force, "force", false, "Skips check of empty target directory, forces overwrite of existing files") - flag.BoolVar(&c.showHelp, "h", false, "Show help information") - flag.BoolVar(&c.showHelp, "help", false, "Show help information") - flag.BoolVar(&c.showVersion, "v", false, "Show version information") - flag.BoolVar(&c.showVersion, "version", false, "Show version information") - - // Custom usage function - flag.Usage = c.printHelp - - // Parse flags - flag.Parse() - - // Check for unrecognized arguments or "help" - if flag.NArg() > 0 { - arg := flag.Arg(0) - if arg == "help" || arg == "?" || strings.HasPrefix(arg, "-") { - c.showHelp = true - } - } - - // Check for "--" prefixed long-form flags - for i, arg := range os.Args { - switch arg { - case "--source": - if i+1 < len(os.Args) { - c.SourceDir = os.Args[i+1] - } - case "--target": - if i+1 < len(os.Args) { - c.TargetDir = os.Args[i+1] - } - case "--help": - c.showHelp = true - case "--version": - c.showVersion = true - } - } -} - -func (c *Config) Evaluate() error { - if c.showHelp { - c.printHelp() - os.Exit(0) - } - - if c.showVersion { - c.printVersion() - os.Exit(0) - } - - return c.validate() -} - -func (c *Config) printHelp() { - fmt.Println("PDFMinion adds page numbers to existing PDF files.") - fmt.Println("It will take all PDF files from the source directory and put the numbered copies into the target directory.") - fmt.Println("Furthermore, it will ensure that every chapter (aka file) starts with an odd number") - fmt.Println("by adding a single blank page to files with an un-even page count.") - fmt.Println("When printed double-sided, every chapter will start on a right side with an odd pagenumber.") - fmt.Println("\n\nUsage:") - fmt.Printf(" -s, --source string\n\tSpecify the source directory (default \"%s\")\n", defaultSourceDir) - fmt.Printf(" -t, --target string\n\tSpecify the target directory (default \"%s\")\n", defaultTargetDir) - fmt.Printf(" --force string\n\tSkips check of empty target directory, forces overwrite of existing files (default false)\n") - fmt.Println(" -h, --help, ?, -?, help\n\tShow this help message") - fmt.Println(" -v, --version\n\tShow version information") -} - -func (c *Config) printVersion() { - fmt.Printf("PDFminion version %s\n", Version) - if BuildTime != "" { - t, err := time.Parse("2006 Jan 02 15:04", BuildTime) - if err == nil { - formattedBuildTime := t.Format("2006 Jan 02 15:04") - parts := strings.Split(formattedBuildTime, " ") - if len(parts) == 4 { - day := parts[2] - suffix := getSuffix(day) - parts[2] = day + suffix - parts[3] += "h" - formattedBuildTime = strings.Join(parts, " ") - } - fmt.Printf("Build time: %s\n", formattedBuildTime) - } else { - fmt.Printf("Build time: %s\n", BuildTime) - } - } else { - fmt.Println("Build time: Not available") - } -} - -func (c *Config) validate() error { - if err := c.validateSourceDir(); err != nil { - return err - } - return c.validateTargetDir() -} - -func (c *Config) validateSourceDir() error { - if _, err := os.Stat(c.SourceDir); os.IsNotExist(err) { - return fmt.Errorf("source directory %s does not exist", c.SourceDir) - } - return nil -} - -func (c *Config) validateTargetDir() error { - if _, err := os.Stat(c.TargetDir); os.IsNotExist(err) { - fmt.Printf("Target directory %s does not exist. Creating it...\n", c.TargetDir) - if err := os.MkdirAll(c.TargetDir, os.ModePerm); err != nil { - return fmt.Errorf("error creating directory %s: %v", c.TargetDir, err) - } - } else if !c.Force { - // Check if the directory is empty - files, err := ioutil.ReadDir(c.TargetDir) - if err != nil { - return fmt.Errorf("error reading directory %s: %v", c.TargetDir, err) - } - - if len(files) > 0 { - return fmt.Errorf("target directory %s is not empty", c.TargetDir) - } - } - return nil -} - -func getSuffix(day string) string { - switch day { - case "01", "21", "31": - return "st" - case "02", "22": - return "nd" - case "03", "23": - return "rd" - default: - return "th" - } -} diff --git a/go-app/internal/pdf/process.go b/go-app/internal/pdf/process.go index f04d38c..6e8123b 100644 --- a/go-app/internal/pdf/process.go +++ b/go-app/internal/pdf/process.go @@ -3,11 +3,11 @@ package pdf import ( "fmt" "log" - "pdfminion/go-app/internal/config" + "pdfminion/internal/cli" "sort" ) -func ProcessPDFs(cfg *config.Config) error { +func ProcessPDFs(cfg *cli.Options) error { InitializePDFInternals() files, err := CollectCandidatePDFs(cfg) diff --git a/go-app/internal/pdf/processFiles.go b/go-app/internal/pdf/processFiles.go index d6d3a28..388cfba 100644 --- a/go-app/internal/pdf/processFiles.go +++ b/go-app/internal/pdf/processFiles.go @@ -9,8 +9,8 @@ import ( "log" "os" "path/filepath" - "pdfminion/go-app/internal/config" - "pdfminion/go-app/internal/util" + "pdfminion/internal/cli" + "pdfminion/internal/util" "strconv" ) @@ -30,7 +30,7 @@ func InitializePDFInternals() { relaxedConf.ValidationMode = model.ValidationRelaxed } -func CollectCandidatePDFs(cfg *config.Config) ([]string, error) { +func CollectCandidatePDFs(cfg *cli.Options) ([]string, error) { // count PDFs in source directory // abort if no PDF file is present @@ -167,10 +167,10 @@ func watermarkConfigurationForFile(chapterNr, previousPageNr, pageCount int) map for page := 1; page <= (pageCount); page++ { var currentPageNr = previousPageNr + page - var chapterStr = config.ChapterPrefix + strconv.Itoa(chapterNr) - var pageStr = config.PageNrPrefix + strconv.Itoa(currentPageNr) + var chapterStr = cli.ChapterPrefix + strconv.Itoa(chapterNr) + var pageStr = cli.PageNrPrefix + strconv.Itoa(currentPageNr) - wmcs[page], _ = api.TextWatermark(chapterStr+config.ChapterPageSeparator+pageStr, + wmcs[page], _ = api.TextWatermark(chapterStr+cli.ChapterPageSeparator+pageStr, waterMarkDescription(currentPageNr), true, false, types.POINTS) } return wmcs diff --git a/go-app/internal/pdf/processSingleFile.go b/go-app/internal/pdf/processSingleFile.go index c142c2f..5671a0d 100644 --- a/go-app/internal/pdf/processSingleFile.go +++ b/go-app/internal/pdf/processSingleFile.go @@ -5,7 +5,7 @@ import ( "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" "log" - "pdfminion/go-app/internal/util" + "pdfminion/internal/util" "strconv" )