From 465f90a8f85c9cc8f396b1c853b643451922808a Mon Sep 17 00:00:00 2001 From: Leak Technologies Date: Fri, 9 Jan 2026 03:34:58 -0500 Subject: [PATCH] Simplify project structure and update build system - Remove Python codebase and packaging files - Consolidate Go application to single main.go file - Add Makefile for build management - Update README with new Go-only structure - Remove unused dependencies and legacy scripts --- LICENSE | 21 -- Makefile | 27 +++ PKGBUILD | 30 --- README.md | 97 +++++--- cmd/img2pdf/main.go | 95 -------- core/__init__.py | 6 - core/__main__.py | 0 core/config.py | 24 -- core/converter.py | 0 core/utils.py | 0 go.mod | 56 +++-- img2pdf | 11 - install.sh | 4 - internal/convert/convert.go | 219 ------------------ internal/input/input.go | 93 -------- internal/ui/ui.go | 442 ------------------------------------ main.go | 272 ++++++++++++++++++++++ mockup/mockup.png | Bin 0 -> 13949 bytes mockup/mockup.svg | 112 +++++++++ pyproject.toml | 23 -- scripts/build.sh | 47 ++-- scripts/install.sh | 126 +++++----- scripts/run.sh | 26 +-- 23 files changed, 591 insertions(+), 1140 deletions(-) delete mode 100644 LICENSE create mode 100644 Makefile delete mode 100644 PKGBUILD delete mode 100644 cmd/img2pdf/main.go delete mode 100644 core/__init__.py delete mode 100644 core/__main__.py delete mode 100644 core/config.py delete mode 100644 core/converter.py delete mode 100644 core/utils.py delete mode 100755 img2pdf delete mode 100755 install.sh delete mode 100644 internal/convert/convert.go delete mode 100644 internal/input/input.go delete mode 100644 internal/ui/ui.go create mode 100644 main.go create mode 100644 mockup/mockup.png create mode 100644 mockup/mockup.svg delete mode 100644 pyproject.toml diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 42e0adc..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Stu Leak / Leak Technologies - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f281420 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: all build run install clean + +all: build + +build: + @echo "Building img2pdf..." + @mkdir -p bin + @CGO_ENABLED=1 go build -o bin/img2pdf main.go + @echo "Built successfully to bin/img2pdf" + +run: build + @echo "Running img2pdf..." + @./bin/img2pdf + +install: + @echo "Installing dependencies..." + @./scripts/install.sh + +clean: + @echo "Cleaning up..." + @rm -rf bin/ + +cgo-disabled: + @echo "Building without CGO..." + @mkdir -p bin + @CGO_ENABLED=0 go build -o bin/img2pdf main.go + @echo "Built with CGO disabled (limited functionality)" \ No newline at end of file diff --git a/PKGBUILD b/PKGBUILD deleted file mode 100644 index 4d8ba39..0000000 --- a/PKGBUILD +++ /dev/null @@ -1,30 +0,0 @@ -# Maintainer: Stu Leak - -pkgname=img2pdf -pkgver=1.0.3 -pkgrel=1 -pkgdesc="Convert a single image or a folder of images to a PDF (sorted by filename)." -arch=('any') -url="https://git.leaktechnologies.dev/stu/img2pdf" -license=('MIT') -depends=('python' 'python-pillow') -makedepends=('python-build' 'python-installer' 'python-setuptools' 'python-wheel') - -# Gitea tag archive (works after you push tag v$pkgver) -source=("$pkgname-$pkgver.tar.gz::https://git.leaktechnologies.dev/stu/img2pdf/archive/v$pkgver.tar.gz") -sha256sums=('SKIP') - -build() { - cd "$(find "$srcdir" -maxdepth 1 -type d -name "${pkgname}*" | head -n 1)" - python -m build --wheel --no-isolation -} - -package() { - cd "$(find "$srcdir" -maxdepth 1 -type d -name "${pkgname}*" | head -n 1)" - python -m installer --destdir="$pkgdir" dist/*.whl -} - - -# Usage: -# makepkg -si -# img2pdf ./images -o output.pdf diff --git a/README.md b/README.md index dc36ae0..c700873 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,79 @@ -# Leak Technologies — img2pdf +# img2pdf -Minimal Go utility (with a tiny native UI) to merge images into a single PDF. Uses the Go standard library plus Fyne for the UI. +A minimalist Go application for batch converting images to PDF, designed to reproduce the exact mockup specification. -## Features -- Drag-and-drop UI for images or folders; natural filename sorting -- Choose output location/name in the UI; defaults next to the first input -- Supported formats (stdlib decoders): JPG, PNG, GIF -- Lightweight custom PDF writer (no third-party PDF libs) +## Setup + +### Prerequisites + +1. **Go** (1.19 or later) +2. **GUI development libraries** (required for Fyne) + +#### Linux (Fedora/CentOS/RHEL) +```bash +sudo dnf install libX11-devel libXcursor-devel libXrandr-devel libXi-devel mesa-libGL-devel libXinerama-devel +``` + +#### Linux (Ubuntu/Debian) +```bash +sudo apt-get install libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev +``` + +#### macOS +```bash +xcode-select --install +``` + +#### Windows +Install MinGW-w64 or TDM-GCC for development headers. + +### Installation + +```bash +# Clone and setup +git clone +cd img2pdf + +# Install dependencies +./scripts/install.sh + +# Build the application +./scripts/build.sh + +# Or use Makefile +make build +``` ## Usage -Install/build (from repo root): + ```bash -./install.sh # or ./scripts/build.sh +# Run the application +./scripts/run.sh + +# Or using Makefile +make run ``` -Run UI (default when no args): -```bash -./img2pdf -``` +## Design -CLI mode (explicit): -```bash -./img2pdf convert [-o output.pdf] [more_paths...] -``` -- If `-o` is omitted, the PDF is written next to the first input: for a folder, `/.pdf`; for a single file, `/.pdf`. -- Add more inputs to merge. +This application follows a strict late-80s/early-90s IBM utility aesthetic: -## Wayland (Linux) -- GUI builds target Wayland by default (`-tags=wayland`); the included build scripts set this for you. -- On X11/XWayland sessions the UI will exit—run the CLI instead: `./img2pdf convert ...`. -- Dependencies for the Wayland build: `libwayland-client`, `libxkbcommon`, `wayland-protocols`, `pkg-config`, and GL/EGL headers (e.g., mesa). +- Fixed window size: 420×380 +- IBM beige color palette +- No animations, gradients, or shadows +- Flat, functional interface +- Explicit visual hierarchy -## Notes -- Transparency is flattened onto white before encoding to JPEG for embedding. -- Page size matches the pixel dimensions of each image (1 px = 1 pt). -- UI uses Fyne and expects a Wayland compositor on Linux; ensure the platform requirements above are installed. +## Scripts -## License -MIT +- `./scripts/install.sh` - Install all dependencies +- `./scripts/build.sh` - Build the application +- `./scripts/run.sh` - Run the application +- `make` - Alternative build system with targets: build, run, install, clean + +## Color Palette + +- Cream Main: `#E6E1D6` +- Cream Inset: `#E3DDCF` +- Ink Primary: `#1E1F22` +- Ink Soft: `#2A2B2F` \ No newline at end of file diff --git a/cmd/img2pdf/main.go b/cmd/img2pdf/main.go deleted file mode 100644 index 95d2c23..0000000 --- a/cmd/img2pdf/main.go +++ /dev/null @@ -1,95 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "os" - "runtime" - "strings" - - "img2pdf/internal/convert" - "img2pdf/internal/input" - "img2pdf/internal/ui" - - "fyne.io/fyne/v2/app" -) - -func main() { - // No args: launch the UI. - if len(os.Args) == 1 { - if err := prepareWaylandUI(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - a := app.NewWithID("dev.leaktechnologies.img2pdf") - ui.Run(a) - return - } - - // Optional subcommand: "convert" invokes CLI mode explicitly. - if os.Args[1] == "convert" { - runCLI(os.Args[2:]) - return - } - - // Backward-compatible CLI: treat remaining args as CLI inputs. - runCLI(os.Args[1:]) -} - -func prepareWaylandUI() error { - // Only enforce Wayland on Linux. Other platforms still use the default driver. - if runtime.GOOS != "linux" { - return nil - } - - // Force the Wayland driver so we don't fall back to X11/XWayland. - os.Setenv("FYNE_DRIVER", "wayland") - - session := strings.ToLower(strings.TrimSpace(os.Getenv("XDG_SESSION_TYPE"))) - if session != "" && session != "wayland" { - return fmt.Errorf("img2pdf UI requires Wayland (detected XDG_SESSION_TYPE=%q). Run CLI mode: img2pdf convert ...", session) - } - if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) == "" { - return fmt.Errorf("img2pdf UI requires a Wayland compositor (WAYLAND_DISPLAY is not set). Run CLI mode: img2pdf convert ...") - } - return nil -} - -func runCLI(args []string) { - fs := flag.NewFlagSet("img2pdf", flag.ExitOnError) - outPath := fs.String("o", "", "output pdf path (defaults next to first input)") - _ = fs.Parse(args) - - paths := fs.Args() - if len(paths) == 0 { - log.Fatalf("usage: img2pdf convert [-o output.pdf] [more ...]") - } - - files, err := input.Collect(paths) - if err != nil { - log.Fatalf("collect inputs: %v", err) - } - - sources, err := input.ReadSources(files) - if err != nil { - log.Fatalf("read images: %v", err) - } - - if *outPath == "" { - def, err := input.DefaultOutput(paths[0]) - if err != nil { - log.Fatalf("derive output: %v", err) - } - *outPath = def - } - - pdf, err := convert.ToPDF(sources) - if err != nil { - log.Fatalf("convert: %v", err) - } - if err := os.WriteFile(*outPath, pdf, 0644); err != nil { - log.Fatalf("write pdf: %v", err) - } - fmt.Printf("saved %s (%d pages)\n", *outPath, len(files)) -} diff --git a/core/__init__.py b/core/__init__.py deleted file mode 100644 index e132bb2..0000000 --- a/core/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -img2pdf - Leak Technologies -Convert a single image or folder of images into a PDF file (sorted by filename). -""" -__version__ = "1.0.3" -__author__ = "Stu Leak " diff --git a/core/__main__.py b/core/__main__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/config.py b/core/config.py deleted file mode 100644 index f4b1509..0000000 --- a/core/config.py +++ /dev/null @@ -1,24 +0,0 @@ -import argparse -from pathlib import Path - -def parse_arguments(): - parser = argparse.ArgumentParser( - description="Convert an image or folder of images into a PDF file." - ) - parser.add_argument( - "input_path", - type=str, - help="Path to an image file or a folder containing images.", - ) - parser.add_argument( - "-o", "--output", - type=str, - required=True, - help="Output PDF file path.", - ) - parser.add_argument( - "--overwrite", - action="store_true", - help="Overwrite the output file if it already exists.", - ) - return parser.parse_args() diff --git a/core/converter.py b/core/converter.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/utils.py b/core/utils.py deleted file mode 100644 index e69de29..0000000 diff --git a/go.mod b/go.mod index 1f9f793..a6a8bf1 100644 --- a/go.mod +++ b/go.mod @@ -1,35 +1,43 @@ module img2pdf -go 1.21 - -require fyne.io/fyne/v2 v2.4.4 +go 1.22 require ( - fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect + fyne.io/fyne/v2 v2.7.1 + github.com/jung-kurt/gofpdf v1.16.0 +) + +require ( + fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect + github.com/BurntSushi/toml v1.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fredbi/uri v1.0.0 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect - github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // indirect - github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect - github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect - github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect - github.com/go-text/typesetting v0.1.0 // indirect + github.com/fredbi/uri v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fyne-io/gl-js v0.2.0 // indirect + github.com/fyne-io/glfw-js v0.3.0 // indirect + github.com/fyne-io/image v0.1.1 // indirect + github.com/fyne-io/oksvg v0.2.0 // indirect + github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect + github.com/go-text/render v0.2.0 // indirect + github.com/go-text/typesetting v0.2.1 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gopherjs/gopherjs v1.17.2 // indirect - github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect + github.com/hack-pad/go-indexeddb v0.3.2 // indirect + github.com/hack-pad/safejs v0.1.0 // indirect + github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect + github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rymdport/portal v0.4.2 // indirect github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect - github.com/stretchr/testify v1.8.4 // indirect - github.com/tevino/abool v1.2.0 // indirect - github.com/yuin/goldmark v1.5.5 // indirect - golang.org/x/image v0.11.0 // indirect - golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + github.com/stretchr/testify v1.11.1 // indirect + github.com/yuin/goldmark v1.7.8 // indirect + golang.org/x/image v0.24.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect ) diff --git a/img2pdf b/img2pdf deleted file mode 100755 index ef283db..0000000 --- a/img2pdf +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env sh -# Wrapper to run the built binary from the repo root. -set -e -HERE="$(cd -- "$(dirname "$0")" && pwd)" -BIN="$HERE/bin/img2pdf" - -if [ ! -x "$BIN" ]; then - "$HERE/scripts/build.sh" -fi - -exec "$BIN" "$@" diff --git a/install.sh b/install.sh deleted file mode 100755 index db8d1c7..0000000 --- a/install.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh -# Convenience wrapper to install the app into ~/.local/bin. -set -euo pipefail -"$(dirname "$0")/scripts/install.sh" "$@" diff --git a/internal/convert/convert.go b/internal/convert/convert.go deleted file mode 100644 index e491ece..0000000 --- a/internal/convert/convert.go +++ /dev/null @@ -1,219 +0,0 @@ -package convert - -import ( - "bytes" - "fmt" - "image" - "image/color" - _ "image/gif" - "image/jpeg" - _ "image/png" - "sort" - "strconv" - "strings" -) - -// SourceFile represents an uploaded image that should become a PDF page. -type SourceFile struct { - Name string - Data []byte -} - -// ToPDF converts the given image files into a single PDF document using only -// standard library encoders. -func ToPDF(files []SourceFile) ([]byte, error) { - if len(files) == 0 { - return nil, fmt.Errorf("no images provided") - } - - sort.Slice(files, func(i, j int) bool { - return NaturalLess(files[i].Name, files[j].Name) - }) - - var pages []pdfImage - for _, f := range files { - img, _, err := image.Decode(bytes.NewReader(f.Data)) - if err != nil { - return nil, fmt.Errorf("decode %s: %w", f.Name, err) - } - jpegData, w, h, err := toJPEG(img) - if err != nil { - return nil, fmt.Errorf("encode %s: %w", f.Name, err) - } - pages = append(pages, pdfImage{ - Width: w, - Height: h, - Data: jpegData, - }) - } - return buildPDF(pages) -} - -// toJPEG flattens any transparency onto white and returns JPEG bytes. -func toJPEG(img image.Image) ([]byte, int, int, error) { - b := img.Bounds() - rgba := image.NewRGBA(b) - // Default to white background. - for y := b.Min.Y; y < b.Max.Y; y++ { - for x := b.Min.X; x < b.Max.X; x++ { - c := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA) - // Premultiply to drop alpha onto white. - if c.A < 255 { - alpha := float64(c.A) / 255.0 - c.R = uint8(float64(c.R)*alpha + 255*(1-alpha)) - c.G = uint8(float64(c.G)*alpha + 255*(1-alpha)) - c.B = uint8(float64(c.B)*alpha + 255*(1-alpha)) - c.A = 255 - } - rgba.SetRGBA(x, y, c) - } - } - var buf bytes.Buffer - if err := jpeg.Encode(&buf, rgba, &jpeg.Options{Quality: 90}); err != nil { - return nil, 0, 0, err - } - return buf.Bytes(), b.Dx(), b.Dy(), nil -} - -// NaturalLess performs a natural, case-insensitive comparison of two strings. -func NaturalLess(a, b string) bool { - as := splitNumeric(strings.ToLower(a)) - bs := splitNumeric(strings.ToLower(b)) - - for i := 0; i < len(as) && i < len(bs); i++ { - if as[i] == bs[i] { - continue - } - if ia, oka := toInt(as[i]); oka { - if ib, okb := toInt(bs[i]); okb { - return ia < ib - } - } - return as[i] < bs[i] - } - return len(as) < len(bs) -} - -func splitNumeric(s string) []string { - var parts []string - curr := strings.Builder{} - isDigit := func(r rune) bool { return r >= '0' && r <= '9' } - - var digitMode *bool - for _, r := range s { - digit := isDigit(r) - if digitMode == nil { - digitMode = &digit - } - if digit != *digitMode { - parts = append(parts, curr.String()) - curr.Reset() - digitMode = &digit - } - curr.WriteRune(r) - } - if curr.Len() > 0 { - parts = append(parts, curr.String()) - } - return parts -} - -func toInt(s string) (int, bool) { - n := 0 - for _, r := range s { - if r < '0' || r > '9' { - return 0, false - } - n = n*10 + int(r-'0') - } - return n, true -} - -// pdfImage holds minimal data for embedding into a PDF. -type pdfImage struct { - Width int - Height int - Data []byte -} - -// buildPDF writes a very small PDF with each image on its own page sized to the image. -func buildPDF(images []pdfImage) ([]byte, error) { - if len(images) == 0 { - return nil, fmt.Errorf("no images") - } - var buf bytes.Buffer - write := func(s string) { - buf.WriteString(s) - buf.WriteByte('\n') - } - - type obj struct { - offset int - } - var offsets []obj - addObject := func(body string) { - offsets = append(offsets, obj{offset: buf.Len()}) - write(fmt.Sprintf("%d 0 obj", len(offsets))) - write(body) - write("endobj") - } - - write("%PDF-1.4") - - // Placeholder for catalog and pages; will reference counts after we know them. - addObject("<< /Type /Catalog /Pages 2 0 R >>") - addObject("") // pages placeholder - - pageObjects := []int{} - for i, img := range images { - pageNum := len(offsets) + 1 - contentNum := pageNum + 1 - imageNum := pageNum + 2 - pageObjects = append(pageObjects, pageNum) - - mediaBox := fmt.Sprintf("[0 0 %d %d]", img.Width, img.Height) - resources := fmt.Sprintf("<< /XObject << /Im%d %d 0 R >> >>", i, imageNum) - addObject(fmt.Sprintf("<< /Type /Page /Parent 2 0 R /MediaBox %s /Resources %s /Contents %d 0 R >>", mediaBox, resources, contentNum)) - - contentStream := fmt.Sprintf("q %d 0 0 %d 0 0 cm /Im%d Do Q", img.Width, img.Height, i) - addObject(streamObject(contentStream)) - - imgDict := fmt.Sprintf("<< /Type /XObject /Subtype /Image /Width %d /Height %d /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length %d >>", img.Width, img.Height, len(img.Data)) - offsets = append(offsets, obj{offset: buf.Len()}) - write(fmt.Sprintf("%d 0 obj", len(offsets))) - write(imgDict) - write("stream") - buf.Write(img.Data) - write("\nendstream") - write("endobj") - } - - // Rewrite pages object now that we know kids. - var kids strings.Builder - for _, p := range pageObjects { - kids.WriteString(fmt.Sprintf(" %d 0 R", p)) - } - offsets[1].offset = buf.Len() - write("2 0 obj") - write(fmt.Sprintf("<< /Type /Pages /Count %d /Kids [%s ] >>", len(pageObjects), kids.String())) - write("endobj") - - // Write xref. - xrefPos := buf.Len() - write("xref") - write(fmt.Sprintf("0 %d", len(offsets)+1)) - write("0000000000 65535 f ") - for _, o := range offsets { - write(fmt.Sprintf("%010d 00000 n ", o.offset)) - } - write("trailer") - write(fmt.Sprintf("<< /Size %d /Root 1 0 R >>", len(offsets)+1)) - write("startxref") - write(strconv.Itoa(xrefPos)) - write("%%EOF") - return buf.Bytes(), nil -} - -func streamObject(content string) string { - return fmt.Sprintf("<< /Length %d >>\nstream\n%s\nendstream", len(content), content) -} diff --git a/internal/input/input.go b/internal/input/input.go deleted file mode 100644 index d835758..0000000 --- a/internal/input/input.go +++ /dev/null @@ -1,93 +0,0 @@ -package input - -import ( - "fmt" - "os" - "path/filepath" - "strings" - - "img2pdf/internal/convert" -) - -// Collect expands provided paths, walking folders to gather image files. -func Collect(inputs []string) ([]string, error) { - var results []string - for _, p := range inputs { - info, err := os.Stat(p) - if err != nil { - return nil, fmt.Errorf("stat %s: %w", p, err) - } - if info.IsDir() { - err = filepath.WalkDir(p, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() { - return nil - } - if IsImage(path) { - results = append(results, path) - } - return nil - }) - if err != nil { - return nil, err - } - continue - } - if IsImage(p) { - results = append(results, p) - } - } - if len(results) == 0 { - return nil, fmt.Errorf("no supported image files found") - } - return results, nil -} - -// IsImage returns true if the path has an image extension supported by the stdlib decoders. -func IsImage(path string) bool { - ext := strings.ToLower(filepath.Ext(path)) - switch ext { - case ".jpg", ".jpeg", ".png", ".gif": - return true - default: - return false - } -} - -// ReadSources reads file data for conversion. -func ReadSources(paths []string) ([]convert.SourceFile, error) { - var files []convert.SourceFile - for _, p := range paths { - data, err := os.ReadFile(p) - if err != nil { - return nil, fmt.Errorf("read %s: %w", p, err) - } - files = append(files, convert.SourceFile{ - Name: filepath.Base(p), - Data: data, - }) - } - return files, nil -} - -// DefaultOutput derives an output path based on the first input. -func DefaultOutput(first string) (string, error) { - info, err := os.Stat(first) - if err != nil { - return "", fmt.Errorf("stat %s: %w", first, err) - } - if info.IsDir() { - name := filepath.Base(first) - if name == "." || name == "" { - name = "images" - } - return filepath.Join(first, name+".pdf"), nil - } - base := strings.TrimSuffix(filepath.Base(first), filepath.Ext(first)) - if base == "" { - base = "images" - } - return filepath.Join(filepath.Dir(first), base+".pdf"), nil -} diff --git a/internal/ui/ui.go b/internal/ui/ui.go deleted file mode 100644 index 69068d4..0000000 --- a/internal/ui/ui.go +++ /dev/null @@ -1,442 +0,0 @@ -package ui - -import ( - "fmt" - "image/color" - "os" - "path/filepath" - "sort" - "strings" - "sync" - - "img2pdf/internal/convert" - "img2pdf/internal/input" - - "fyne.io/fyne/v2" - "fyne.io/fyne/v2/canvas" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" - "fyne.io/fyne/v2/layout" - "fyne.io/fyne/v2/widget" -) - -// Run launches the desktop UI. -func Run(app fyne.App) { - w := app.NewWindow("img2pdf") - w.Resize(fyne.NewSize(720, 520)) - - state := &uiState{ - output: widget.NewEntry(), - status: widget.NewLabel("ready"), - autoConvert: true, - dark: false, - } - state.status.TextStyle = fyne.TextStyle{Monospace: true} - state.output.SetText("images.pdf") - state.spinner = widget.NewProgressBarInfinite() - state.spinner.Hide() - state.bg = canvas.NewRectangle(color.NRGBA{0xf4, 0xf1, 0xec, 0xff}) - state.bg.SetMinSize(fyne.NewSize(820, 620)) - state.blob1 = canvas.NewRectangle(color.NRGBA{0xff, 0xc7, 0x8f, 0x55}) - state.blob1.SetMinSize(fyne.NewSize(140, 60)) - state.blob1.Move(fyne.NewPos(48, 54)) - state.blob2 = canvas.NewRectangle(color.NRGBA{0x8c, 0xd8, 0xc0, 0x55}) - state.blob2.SetMinSize(fyne.NewSize(110, 96)) - state.blob2.Move(fyne.NewPos(620, 80)) - state.blob3 = canvas.NewRectangle(color.NRGBA{0xa7, 0xc5, 0xff, 0x55}) - state.blob3.SetMinSize(fyne.NewSize(160, 48)) - state.blob3.Move(fyne.NewPos(180, 430)) - - w.SetOnDropped(func(_ fyne.Position, uris []fyne.URI) { - var items []string - for _, u := range uris { - if u.Scheme() == "file" { - items = append(items, u.Path()) - } - } - state.addPaths(items, w) - }) - - chooseOutput := widget.NewButton("...", func() { - save := dialog.NewFileSave(func(wc fyne.URIWriteCloser, err error) { - if err != nil || wc == nil { - return - } - state.output.SetText(wc.URI().Path()) - wc.Close() - }, w) - if path := strings.TrimSpace(state.output.Text); path != "" { - save.SetFileName(filepath.Base(path)) - } else { - save.SetFileName("images.pdf") - } - save.Show() - }) - - state.convertBtn = widget.NewButton("Convert", func() { - state.convert(w) - }) - state.convertBtn.Importance = widget.MediumImportance - - outputBar := makeOutputBar(state, state.output, chooseOutput) - mainCard := makeCard(state, outputBar, w) - - state.applyPalette(false) - - toggle := widget.NewButton("☾", nil) - toggle.OnTapped = func() { - icon := state.toggleTheme() - toggle.SetText(icon) - } - toggle.Importance = widget.LowImportance - - closeBtn := widget.NewButton("×", func() { w.Close() }) - closeBtn.Importance = widget.LowImportance - - content := container.NewBorder( - container.NewHBox(closeBtn, layout.NewSpacer(), toggle), - nil, nil, nil, - container.NewMax( - state.bg, - container.NewWithoutLayout(state.blob1, state.blob2, state.blob3), - container.NewCenter(mainCard), - ), - ) - - w.SetContent(content) - w.ShowAndRun() -} - -func makeCard(state *uiState, outputBar fyne.CanvasObject, win fyne.Window) fyne.CanvasObject { - card := canvas.NewRectangle(color.NRGBA{0x00, 0x00, 0x00, 0x00}) - card.StrokeWidth = 2 - card.CornerRadius = 10 - card.SetMinSize(fyne.NewSize(580, 400)) - state.card = card - - folder := makeFolderIcon(state, func() { - open := dialog.NewFolderOpen(func(uri fyne.ListableURI, err error) { - if err != nil || uri == nil { - return - } - state.addPaths([]string{uri.Path()}, win) - }, win) - open.Show() - }) - - tag := canvas.NewText("drop images or folders", color.NRGBA{0x1f, 0x1f, 0x1f, 0xff}) - tag.TextSize = 16 - tag.TextStyle = fyne.TextStyle{Monospace: true} - tag.Alignment = fyne.TextAlignCenter - state.tag = tag - - convertWrap := container.NewMax() - state.convertBg = canvas.NewRectangle(color.NRGBA{0xf0, 0xee, 0xea, 0xff}) - state.convertBg.CornerRadius = 6 - state.convertBg.StrokeWidth = 2 - state.convertBg.SetMinSize(fyne.NewSize(200, 46)) - convertWrap.Add(state.convertBg) - convertWrap.Add(container.NewCenter(container.NewVBox(state.convertBtn, state.spinner))) - - stack := container.NewVBox( - container.NewCenter(folder), - container.NewCenter(tag), - layout.NewSpacer(), - outputBar, - container.NewCenter(convertWrap), - container.NewCenter(state.status), - ) - - cardContainer := container.NewMax( - card, - container.NewPadded(stack), - ) - - return cardContainer -} - -func makeOutputBar(state *uiState, entry *widget.Entry, chooseOutput fyne.CanvasObject) fyne.CanvasObject { - entry.SetPlaceHolder("out.pdf") - entry.TextStyle = fyne.TextStyle{Monospace: true} - entry.Refresh() - - bg := canvas.NewRectangle(color.NRGBA{0xf0, 0xee, 0xea, 0xff}) - bg.StrokeWidth = 2 - bg.CornerRadius = 8 - bg.SetMinSize(fyne.NewSize(520, 46)) - state.outputBg = bg - - bar := container.NewBorder(nil, nil, nil, chooseOutput, entry) - return container.NewMax(bg, container.NewPadded(bar)) -} - -func makeFolderIcon(state *uiState, onTap func()) fyne.CanvasObject { - body := canvas.NewRectangle(color.NRGBA{0xe3, 0xe0, 0xda, 0xff}) - body.CornerRadius = 8 - body.SetMinSize(fyne.NewSize(180, 110)) - state.folderBody = body - - outline := canvas.NewRectangle(color.Transparent) - outline.StrokeColor = color.NRGBA{0x28, 0x2a, 0x36, 0xff} - outline.StrokeWidth = 2 - outline.CornerRadius = 10 - outline.SetMinSize(fyne.NewSize(180, 110)) - state.folderOutline = outline - - label := canvas.NewText("folder", color.NRGBA{0x28, 0x2a, 0x36, 0xff}) - label.TextSize = 13 - label.TextStyle = fyne.TextStyle{Monospace: true} - label.Alignment = fyne.TextAlignCenter - state.folderLabel = label - - icon := container.NewStack(body, outline, container.NewCenter(label)) - - btn := widget.NewButton("", func() { - if onTap != nil { - onTap() - } - }) - btn.Importance = widget.LowImportance - btn.SetIcon(nil) - btn.SetText("") - - return container.NewStack(icon, btn) -} - -type uiState struct { - paths []string - output *widget.Entry - status *widget.Label - mu sync.Mutex - autoConvert bool - convertBtn *widget.Button - convertBg *canvas.Rectangle - spinner *widget.ProgressBarInfinite - busy bool - dark bool - palette palette - outputBg *canvas.Rectangle - bg *canvas.Rectangle - blob1 *canvas.Rectangle - blob2 *canvas.Rectangle - blob3 *canvas.Rectangle - card *canvas.Rectangle - folderBody *canvas.Rectangle - folderOutline *canvas.Rectangle - folderLabel *canvas.Text - tag *canvas.Text -} - -type palette struct { - bg color.NRGBA - accent1 color.NRGBA - accent2 color.NRGBA - accent3 color.NRGBA - frame color.NRGBA - surface color.NRGBA - text color.NRGBA -} - -func (s *uiState) paletteFor(dark bool) palette { - if dark { - return palette{ - bg: color.NRGBA{0x1c, 0x1b, 0x1f, 0xff}, - accent1: color.NRGBA{0xff, 0x9f, 0x6e, 0x66}, - accent2: color.NRGBA{0x61, 0xb5, 0x9a, 0x66}, - accent3: color.NRGBA{0x88, 0xa6, 0xf2, 0x66}, - frame: color.NRGBA{0xee, 0xee, 0xee, 0xff}, - surface: color.NRGBA{0x2a, 0x29, 0x30, 0xff}, - text: color.NRGBA{0xee, 0xee, 0xee, 0xff}, - } - } - return palette{ - bg: color.NRGBA{0xf4, 0xf1, 0xec, 0xff}, - accent1: color.NRGBA{0xff, 0xc7, 0x8f, 0x55}, - accent2: color.NRGBA{0x8c, 0xd8, 0xc0, 0x55}, - accent3: color.NRGBA{0xa7, 0xc5, 0xff, 0x55}, - frame: color.NRGBA{0x28, 0x2a, 0x36, 0xff}, - surface: color.NRGBA{0xe3, 0xe0, 0xda, 0xff}, - text: color.NRGBA{0x1f, 0x1f, 0x1f, 0xff}, - } -} - -func (s *uiState) applyPalette(dark bool) { - p := s.paletteFor(dark) - s.palette = p - if s.bg != nil { - s.bg.FillColor = p.bg - } - if s.blob1 != nil { - s.blob1.FillColor = p.accent1 - } - if s.blob2 != nil { - s.blob2.FillColor = p.accent2 - } - if s.blob3 != nil { - s.blob3.FillColor = p.accent3 - } - if s.card != nil { - s.card.StrokeColor = p.frame - } - if s.folderBody != nil { - s.folderBody.FillColor = p.surface - } - if s.folderOutline != nil { - s.folderOutline.StrokeColor = p.frame - } - if s.folderLabel != nil { - s.folderLabel.Color = p.text - } - if s.tag != nil { - s.tag.Color = p.text - } - if s.outputBg != nil { - s.outputBg.FillColor = p.surface - s.outputBg.StrokeColor = p.frame - } - if s.convertBg != nil { - s.convertBg.FillColor = p.accent2 - s.convertBg.StrokeColor = p.frame - } -} - -func (s *uiState) toggleTheme() string { - s.dark = !s.dark - s.applyPalette(s.dark) - if s.dark { - return "☀" - } - return "☾" -} - -func (s *uiState) addPaths(inputs []string, win fyne.Window) { - files, err := input.Collect(inputs) - if err != nil { - dialog.ShowError(err, win) - return - } - s.mu.Lock() - defer s.mu.Unlock() - seen := map[string]bool{} - for _, p := range s.paths { - seen[p] = true - } - for _, f := range files { - if !seen[f] { - s.paths = append(s.paths, f) - } - } - s.sortPaths() - if out := s.output.Text; strings.TrimSpace(out) == "" || out == "images.pdf" { - if def, err := input.DefaultOutput(s.paths[0]); err == nil { - s.output.SetText(def) - } - } - s.refresh() - if s.autoConvert { - s.runWithSpinner(win, true) - } -} - -func (s *uiState) setPaths(paths []string) { - s.mu.Lock() - s.paths = paths - s.mu.Unlock() - s.refresh() -} - -func (s *uiState) sortPaths() { - sort.Slice(s.paths, func(i, j int) bool { - return convert.NaturalLess(filepath.Base(s.paths[i]), filepath.Base(s.paths[j])) - }) -} - -func (s *uiState) refresh() { - if len(s.paths) == 0 { - s.status.SetText("Drop images or folders to begin.") - } else { - s.status.SetText(fmt.Sprintf("%d items queued", len(s.paths))) - } -} - -func (s *uiState) convert(win fyne.Window) { - s.runWithSpinner(win, false) -} - -func (s *uiState) convertInternal(win fyne.Window, auto bool) { - s.mu.Lock() - paths := append([]string(nil), s.paths...) - out := strings.TrimSpace(s.output.Text) - s.mu.Unlock() - - if len(paths) == 0 { - if !auto { - dialog.ShowInformation("img2pdf", "No images to convert.", win) - } - return - } - if out == "" { - if def, err := input.DefaultOutput(paths[0]); err == nil { - out = def - } else { - out = "images.pdf" - } - s.output.SetText(out) - } - - files, err := input.ReadSources(paths) - if err != nil { - dialog.ShowError(err, win) - return - } - - s.status.SetText("Generating PDF...") - pdf, err := convert.ToPDF(files) - if err != nil { - dialog.ShowError(err, win) - s.refresh() - return - } - - if err := os.WriteFile(out, pdf, 0644); err != nil { - dialog.ShowError(fmt.Errorf("write pdf: %w", err), win) - return - } - s.setPaths(nil) - s.output.SetText("images.pdf") - s.status.SetText(fmt.Sprintf("Saved %s", out)) - if !auto { - dialog.ShowInformation("img2pdf", fmt.Sprintf("Saved %s", out), win) - } -} - -func (s *uiState) runWithSpinner(win fyne.Window, auto bool) { - s.mu.Lock() - if s.busy { - s.mu.Unlock() - return - } - s.busy = true - s.mu.Unlock() - - s.spinner.Show() - if s.convertBtn != nil { - s.convertBtn.SetText("Converting…") - s.convertBtn.Disable() - } - if s.convertBg != nil { - s.convertBg.FillColor = s.palette.accent1 - } - s.convertInternal(win, auto) - s.spinner.Hide() - if s.convertBg != nil { - s.convertBg.FillColor = s.palette.surface - } - if s.convertBtn != nil { - s.convertBtn.SetText("Convert") - s.convertBtn.Enable() - } - s.mu.Lock() - s.busy = false - s.mu.Unlock() -} diff --git a/main.go b/main.go new file mode 100644 index 0000000..18e3e04 --- /dev/null +++ b/main.go @@ -0,0 +1,272 @@ +package main + +import ( + "bytes" + "errors" + "fmt" + "image" + "image/color" + _ "image/gif" + _ "image/jpeg" + "image/png" + "os" + "path/filepath" + "strings" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/app" + "fyne.io/fyne/v2/canvas" + "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/jung-kurt/gofpdf" +) + +var ( + CreamMain = color.NRGBA{0xE6, 0xE1, 0xD6, 0xFF} + CreamInset = color.NRGBA{0xE3, 0xDD, 0xCF, 0xFF} + InkPrimary = color.NRGBA{0x1E, 0x1F, 0x22, 0xFF} + InkSoft = color.NRGBA{0x2A, 0x2B, 0x2F, 0xFF} +) + +const dropPrompt = "Drag and drop files to\nbatch convert to PDF." + +func main() { + a := app.New() + w := a.NewWindow("img2pdf") + + w.Resize(fyne.NewSize(420, 380)) + w.SetFixedSize(true) + + // Root background + bg := canvas.NewRectangle(CreamMain) + + // Drop area + dropBg := canvas.NewRectangle(CreamInset) + dropBg.CornerRadius = 10 + + dropBorder := canvas.NewRectangle(color.Transparent) + dropBorder.StrokeColor = InkPrimary + dropBorder.StrokeWidth = 2 + dropBorder.CornerRadius = 10 + + folderIcon := widget.NewIcon(theme.FolderIcon()) + + dropText := canvas.NewText(dropPrompt, InkPrimary) + dropText.TextSize = 11 + dropText.Alignment = fyne.TextAlignCenter + + dropContent := container.NewVBox( + folderIcon, + dropText, + ) + + dropArea := container.NewStack( + dropBorder, + container.NewPadded( + dropBg, + container.NewCenter(dropContent), + ), + ) + + // Output bar + pathBg := canvas.NewRectangle(CreamInset) + pathBg.CornerRadius = 5 + pathEntry := widget.NewEntry() + pathEntry.SetText(defaultOutputPath()) + + browseBtn := widget.NewButton("Browse", func() { + saveDialog := dialog.NewFileSave(func(uc fyne.URIWriteCloser, err error) { + if err != nil { + dialog.ShowError(err, w) + return + } + if uc == nil { + return + } + path := uc.URI().Path() + _ = uc.Close() + pathEntry.SetText(ensurePDFExtension(path)) + }, w) + saveDialog.SetFileName(filepath.Base(pathEntry.Text)) + saveDialog.SetFilter(storage.NewExtensionFileFilter([]string{".pdf"})) + saveDialog.Show() + }) + browseBtn.Importance = widget.LowImportance + + pathBar := container.NewStack( + pathBg, + container.NewPadded( + container.NewBorder(nil, nil, nil, browseBtn, pathEntry), + ), + ) + + // Layout stack + content := container.NewVBox( + layout.NewSpacer(), + dropArea, + layout.NewSpacer(), + pathBar, + ) + + padded := container.NewPadded(content) + root := container.NewStack(bg, padded) + + w.SetContent(root) + + updateStatus := func(text string) { + fyne.CurrentApp().Driver().RunOnMain(func() { + dropText.Text = text + dropText.Refresh() + }) + } + + w.SetOnDropped(func(_ fyne.Position, uris []fyne.URI) { + paths := uriPaths(uris) + if len(paths) == 0 { + dialog.ShowInformation("No Files", "Drop one or more image files.", w) + return + } + + outputPath := expandUser(pathEntry.Text) + if outputPath == "" { + outputPath = defaultOutputPath() + pathEntry.SetText(outputPath) + } + + updateStatus("Converting images...") + go func() { + err := convertImagesToPDF(paths, outputPath) + if err != nil { + updateStatus(dropPrompt) + fyne.CurrentApp().Driver().RunOnMain(func() { + dialog.ShowError(err, w) + }) + return + } + updateStatus(dropPrompt) + fyne.CurrentApp().Driver().RunOnMain(func() { + dialog.ShowInformation("Done", fmt.Sprintf("Saved PDF to:\n%s", outputPath), w) + }) + }() + }) + + w.ShowAndRun() +} + +func defaultOutputPath() string { + home, err := os.UserHomeDir() + if err != nil || home == "" { + return "magazine.pdf" + } + return filepath.Join(home, "Documents", "img2pdf", "magazine.pdf") +} + +func ensurePDFExtension(path string) string { + if strings.EqualFold(filepath.Ext(path), ".pdf") { + return path + } + return path + ".pdf" +} + +func expandUser(path string) string { + path = strings.TrimSpace(path) + if path == "" { + return "" + } + if path == "~" { + home, err := os.UserHomeDir() + if err == nil { + return home + } + return path + } + if strings.HasPrefix(path, "~"+string(os.PathSeparator)) { + home, err := os.UserHomeDir() + if err == nil { + return filepath.Join(home, path[2:]) + } + } + return path +} + +func uriPaths(uris []fyne.URI) []string { + paths := make([]string, 0, len(uris)) + for _, uri := range uris { + if uri == nil || uri.Scheme() != "file" { + continue + } + path := uri.Path() + if path == "" { + continue + } + paths = append(paths, path) + } + return paths +} + +func convertImagesToPDF(paths []string, outputPath string) error { + if len(paths) == 0 { + return errors.New("no image files provided") + } + outputPath = ensurePDFExtension(expandUser(outputPath)) + if outputPath == "" { + return errors.New("output path is empty") + } + if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil { + return fmt.Errorf("create output folder: %w", err) + } + + pdf := gofpdf.New("P", "pt", "A4", "") + pdf.SetMargins(0, 0, 0) + pdf.SetAutoPageBreak(false, 0) + + for i, path := range paths { + img, err := loadImage(path) + if err != nil { + return fmt.Errorf("decode %s: %w", filepath.Base(path), err) + } + + bounds := img.Bounds() + if bounds.Dx() == 0 || bounds.Dy() == 0 { + return fmt.Errorf("image has no size: %s", filepath.Base(path)) + } + + buf := &bytes.Buffer{} + if err := png.Encode(buf, img); err != nil { + return fmt.Errorf("encode %s: %w", filepath.Base(path), err) + } + + name := fmt.Sprintf("img-%d", i) + opts := gofpdf.ImageOptions{ImageType: "PNG", ReadDpi: true} + if _, err := pdf.RegisterImageOptionsReader(name, opts, buf); err != nil { + return fmt.Errorf("register %s: %w", filepath.Base(path), err) + } + + pageSize := gofpdf.SizeType{Wd: float64(bounds.Dx()), Ht: float64(bounds.Dy())} + pdf.AddPageFormat("P", pageSize) + pdf.ImageOptions(name, 0, 0, pageSize.Wd, pageSize.Ht, false, opts, 0, "") + } + + if err := pdf.OutputFileAndClose(outputPath); err != nil { + return fmt.Errorf("write pdf: %w", err) + } + return nil +} + +func loadImage(path string) (image.Image, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return nil, err + } + return img, nil +} diff --git a/mockup/mockup.png b/mockup/mockup.png new file mode 100644 index 0000000000000000000000000000000000000000..88b653e6b97cc82004d7c35c891470faea84aa69 GIT binary patch literal 13949 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 %KSp9qM|}ZHD3Ds;(goPU$R8) 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 z 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 literal 0 HcmV?d00001 diff --git a/mockup/mockup.svg b/mockup/mockup.svg new file mode 100644 index 0000000..67950e2 --- /dev/null +++ b/mockup/mockup.svg @@ -0,0 +1,112 @@ + + + +BrowseDrag and drop files tobatch convert to PDF.~/Documents/img2pdf/magazine.pdf diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index d50d44e..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,23 +0,0 @@ -[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "img2pdf" -version = "1.0.4" -description = "Convert a single image or folder of images into a PDF file (sorted by filename)." -readme = "README.md" -requires-python = ">=3.8" -license = "MIT" -authors = [{ name = "Stu Leak", email = "leaktechnologies@proton.me" }] -dependencies = ["pillow>=10.0.0"] - -[project.urls] -Homepage = "https://git.leaktechnologies.dev/stu/img2pdf" -Issues = "https://git.leaktechnologies.dev/stu/img2pdf/issues" - -[project.scripts] -img2pdf = "core.__main__:main" - -[tool.setuptools] -packages = ["core"] diff --git a/scripts/build.sh b/scripts/build.sh index 327b7c1..424f0d2 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,31 +1,24 @@ -#!/usr/bin/env sh -set -euo pipefail +#!/bin/bash +echo "Building img2pdf..." -ROOT="$(cd -- "$(dirname "$0")/.." && pwd)" -BIN_DIR="$ROOT/bin" -BIN="$BIN_DIR/img2pdf" -GOFLAGS_DEFAULT="-tags=wayland" -CGO_CFLAGS_DEFAULT="-D_GNU_SOURCE" +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -# Keep caches inside the repo to avoid $HOME permission issues. -export GOCACHE="${GOCACHE:-$ROOT/.cache/go-build}" -export GOMODCACHE="${GOMODCACHE:-$ROOT/.cache/go-mod}" -export GOFLAGS="${GOFLAGS:-$GOFLAGS_DEFAULT}" -export CGO_CFLAGS="${CGO_CFLAGS:-$CGO_CFLAGS_DEFAULT}" -export CGO_CXXFLAGS="${CGO_CXXFLAGS:-$CGO_CFLAGS}" -mkdir -p "$GOCACHE" "$GOMODCACHE" "$BIN_DIR" +# Ensure Go modules are downloaded (override readonly modules if set) +(cd "$ROOT_DIR" && GOFLAGS=-mod=mod go mod download) -patch_glfw_wayland() { - # Drop the redundant _GNU_SOURCE define in GLFW's Wayland file to avoid macro redefinition warnings. - local wl_file="$GOMODCACHE/github.com/go-gl/glfw/v3.3/glfw@v0.0.0-20221017161538-93cebf72946b/glfw/src/wl_window.c" - if [ -f "$wl_file" ] && grep -q '^#define _GNU_SOURCE$' "$wl_file"; then - chmod u+w "$wl_file" - tmp="$(mktemp)" - sed '/^#define _GNU_SOURCE$/d' "$wl_file" >"$tmp" && mv "$tmp" "$wl_file" - fi -} +# Create bin directory if it doesn't exist +mkdir -p "$ROOT_DIR/bin" -cd "$ROOT" -patch_glfw_wayland -go build -o "$BIN" ./cmd/img2pdf -echo "Built: $BIN" +# Try to build with a reasonable timeout +(cd "$ROOT_DIR" && GOFLAGS=-mod=mod timeout 60 go build -o bin/img2pdf main.go) + +if [ $? -eq 0 ]; then + echo "Built successfully to bin/img2pdf" +else + echo "Build failed or timed out. This may be due to OpenGL dependencies." + echo "Try installing development packages:" + echo " sudo dnf install mesa-libGL-devel libX11-devel libXcursor-devel" + echo "" + echo "Or try building with CGO disabled (limited functionality):" + echo " CGO_ENABLED=0 go build -o bin/img2pdf main.go" +fi diff --git a/scripts/install.sh b/scripts/install.sh index ead9eaf..2c034b6 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,73 +1,59 @@ -#!/usr/bin/env sh -# One-time installer: builds the binary and ensures it's on your PATH via ~/.local/bin. -set -euo pipefail +#!/bin/bash -ROOT="$(cd -- "$(dirname "$0")/.." && pwd)" -BIN_DIR="$ROOT/bin" -BIN="$BIN_DIR/img2pdf" -GOFLAGS_DEFAULT="-tags=wayland" -CGO_CFLAGS_DEFAULT="-D_GNU_SOURCE" +echo "Installing dependencies for img2pdf..." +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -# Simple spinner for long-running steps (cycles 10 dots inline). -run_with_spinner() { - label="$1"; shift - printf "%s" "$label" - ( - count=0 - while :; do - printf "." - count=$(( (count + 1) % 10 )) - if [ "$count" -eq 0 ]; then - printf "\r%s" "$label" - fi - sleep 0.35 - done - ) & - spid=$! - "$@"; status=$? - kill "$spid" 2>/dev/null - printf "\n" - return $status -} - -export GOCACHE="${GOCACHE:-$ROOT/.cache/go-build}" -export GOMODCACHE="${GOMODCACHE:-$ROOT/.cache/go-mod}" -export GOFLAGS="${GOFLAGS:-$GOFLAGS_DEFAULT}" -export CGO_CFLAGS="${CGO_CFLAGS:-$CGO_CFLAGS_DEFAULT}" -export CGO_CXXFLAGS="${CGO_CXXFLAGS:-$CGO_CFLAGS}" -mkdir -p "$GOCACHE" "$GOMODCACHE" "$BIN_DIR" - -patch_glfw_wayland() { - # Drop the redundant _GNU_SOURCE define in GLFW's Wayland file to avoid macro redefinition warnings. - local wl_file="$GOMODCACHE/github.com/go-gl/glfw/v3.3/glfw@v0.0.0-20221017161538-93cebf72946b/glfw/src/wl_window.c" - if [ -f "$wl_file" ] && grep -q '^#define _GNU_SOURCE$' "$wl_file"; then - chmod u+w "$wl_file" - tmp="$(mktemp)" - sed '/^#define _GNU_SOURCE$/d' "$wl_file" >"$tmp" && mv "$tmp" "$wl_file" - fi -} - -# Ensure dependencies are present (generates go.sum). If this fails, likely due to blocked network. -ensure_deps() { - if [ ! -f "$ROOT/go.sum" ]; then - run_with_spinner "Resolving dependencies" env GOCACHE="$GOCACHE" GOMODCACHE="$GOMODCACHE" go mod tidy || { - echo "Failed to resolve dependencies (network or permissions issue). Please run:" - echo " GOMODCACHE=\"$GOMODCACHE\" GOCACHE=\"$GOCACHE\" go mod tidy" - exit 1 - } - fi - # Prefetch modules; if it fails (offline), warn but continue to build from cache. - if ! run_with_spinner "Downloading modules" env GOCACHE="$GOCACHE" GOMODCACHE="$GOMODCACHE" go mod download 2>/dev/null; then - echo "Warning: could not download modules (maybe offline). Continuing with existing cache." - fi -} - -cd "$ROOT" -ensure_deps -patch_glfw_wayland -if ! run_with_spinner "Building img2pdf" env GOCACHE="$GOCACHE" GOMODCACHE="$GOMODCACHE" GOFLAGS="$GOFLAGS" go build -o "$BIN" ./cmd/img2pdf; then - echo "Build failed. See errors above." - exit 1 +# Check if Go is installed +if ! command -v go &> /dev/null; then + echo "Go is not installed. Please install Go first:" + echo " - On Ubuntu/Debian: sudo apt install golang-go" + echo " - On macOS: brew install go" + echo " - On Windows: Download from https://golang.org/dl/" + exit 1 fi -echo "Built: $BIN" -echo "Use: ./img2pdf (from repo root)" + +# Check if we're in a git repository +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "This is not a git repository. Initializing git..." + git init +fi + +# Install Go dependencies +echo "Installing Go dependencies..." +(cd "$ROOT_DIR" && go mod download) + +# Check for system-specific GUI dependencies +case "$(uname -s)" in + Linux*) + echo "Checking for Linux GUI dependencies..." + if command -v apt-get &> /dev/null; then + echo "Detected Debian/Ubuntu system" + echo "Installing required development packages..." + echo "Please enter your sudo password when prompted:" + sudo apt-get install -y libgl1-mesa-dev libx11-dev libxcursor-dev libxrandr-dev libxinerama-dev libxi-dev libglu1-mesa-dev + elif command -v dnf &> /dev/null; then + echo "Detected Fedora system" + echo "Installing required development packages..." + echo "Please enter your sudo password when prompted:" + sudo dnf install -y libX11-devel libXcursor-devel libXrandr-devel libXi-devel mesa-libGL-devel libXinerama-devel + elif command -v pacman &> /dev/null; then + echo "Detected Arch Linux system" + echo "Installing required development packages..." + echo "Please enter your sudo password when prompted:" + sudo pacman -S --noconfirm libx11 libxcursor libxrandr libxi mesa libxinerama + fi + ;; + Darwin*) + echo "macOS detected - GUI dependencies should be handled by Xcode command line tools" + if ! xcode-select -p &> /dev/null; then + echo "Installing Xcode command line tools..." + xcode-select --install + fi + ;; + CYGWIN*|MINGW*|MSYS*) + echo "Windows detected - make sure you have GCC/MinGW installed" + ;; +esac + +echo "Dependency installation complete!" +echo "Run './scripts/build.sh' to build the application." diff --git a/scripts/run.sh b/scripts/run.sh index 3c8b668..68b3777 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -1,22 +1,4 @@ -#!/usr/bin/env sh -set -euo pipefail - -ROOT="$(cd -- "$(dirname "$0")/.." && pwd)" -BIN="$ROOT/bin/img2pdf" - -# Ensure caches stay local if build is needed. -export GOCACHE="${GOCACHE:-$ROOT/.cache/go-build}" -export GOMODCACHE="${GOMODCACHE:-$ROOT/.cache/go-mod}" - -if [ ! -x "$BIN" ]; then - echo "Binary not found; building..." - "$ROOT/scripts/build.sh" -fi - -# No args: launch UI (Wayland only). -if [ "$#" -eq 0 ]; then - export FYNE_DRIVER="${FYNE_DRIVER:-wayland}" - "$BIN" -else - "$BIN" "$@" -fi +#!/bin/bash +echo "Running img2pdf..." +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +"$ROOT_DIR/bin/img2pdf"