From cca59b44d17dd31416f32d6d93a90a51eb2df7ff Mon Sep 17 00:00:00 2001 From: Vadim Pidoshva Date: Sun, 22 Feb 2026 22:57:43 -0700 Subject: [PATCH] prod ready --- LICENSE | 21 ++++++ Makefile | 7 ++ README.md | 100 +++++++++++++++++++++++-- VERSION | 2 +- gifify.sh | 209 ++++++++++++++++++++++++++++++++++++++++++++++------- install.sh | 162 ++++++++++++++++++++++++++++------------- 6 files changed, 417 insertions(+), 84 deletions(-) create mode 100644 LICENSE create mode 100644 Makefile diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..279e372 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 pidoshva + +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..74baeca --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +.PHONY: install uninstall + +install: + @./install.sh + +uninstall: + @./install.sh --uninstall diff --git a/README.md b/README.md index 73a0e87..c8de6c5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,101 @@ -# Under Development -# Installation +# gifify -Run the following command to install **gifify**: +Convert videos to optimized GIFs using ffmpeg's two-pass palette technique. + +gifify produces high-quality GIFs with accurate colors by generating an optimal color palette from the source video, then using it to create the final GIF. + +## Features + +- Two-pass palette generation for accurate colors +- Multiple resolution presets (1080p, 720p, 480p, 360p) +- Configurable frame rate +- Custom output path support +- Cross-platform ffmpeg detection and installation +- Safe temp files with automatic cleanup +- Works as a shell function (sourced) or standalone script + +## Install + +**One-liner (curl):** + +```bash +bash -c "$(curl -fsSL https://raw.githubusercontent.com/pidoshva/gifify/main/install.sh)" +``` + +**From a local clone:** + +```bash +git clone https://github.com/pidoshva/gifify.git +cd gifify +./install.sh +``` + +**Or with make:** + +```bash +make install +``` + +The installer copies `gifify.sh` to `~/.gifify/` and adds a `source` line to your `.bashrc` or `.zshrc` (auto-detected). If `ffmpeg` is not installed, it will be installed automatically using your system's package manager. + +After installing, open a new terminal or run: ```bash -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/pidoshva/gifify/main/install.sh)" +source ~/.bashrc # or source ~/.zshrc ``` +## Usage + +```bash +gifify video.mp4 # Default: 1080p, 15fps +gifify video.mp4 --720p # 720p resolution +gifify video.mp4 --480p --fps 10 # 480p at 10fps +gifify video.mp4 -o output.gif # Custom output path +gifify video.mp4 --360p -o small.gif # 360p with custom output +``` + +## Options + +| Flag | Description | Default | +|------|-------------|---------| +| `-o, --output ` | Output file path | `~/Desktop/gifified/.gif` | +| `--fps ` | Frames per second | `15` | +| `--1080p` | Scale to 1920px width | (default) | +| `--720p` | Scale to 1280px width | | +| `--480p` | Scale to 854px width | | +| `--360p` | Scale to 640px width | | +| `-h, --help` | Show help | | +| `-v, --version` | Show version | | + +## How It Works + +1. **Palette generation** -- ffmpeg analyzes the video and generates an optimal 256-color palette using the Lanczos scaling algorithm +2. **GIF creation** -- ffmpeg re-encodes the video using the generated palette for maximum color accuracy + +This two-pass approach produces significantly better results than a single-pass conversion. + +## Requirements + +- **ffmpeg** -- the installer handles this automatically, or install it manually: + - macOS: `brew install ffmpeg` + - Ubuntu/Debian: `sudo apt-get install ffmpeg` + - Fedora: `sudo dnf install ffmpeg` + - Arch: `sudo pacman -S ffmpeg` + +## Output + +By default, GIFs are saved to `~/Desktop/gifified/`. Use `-o` to specify a custom path. + +## Uninstall + +```bash +./install.sh --uninstall +# or +make uninstall +``` + +This removes `~/.gifify/` and cleans the source line from your shell RC files. + +## License -The script checks for `ffmpeg` and installs it if missing (using Homebrew or `apt-get`). It downloads the latest `gifify.sh` and adds it to your `~/.zshrc` so you can start using the function in new terminals. The installer stores a version file in `~/.gifify` and will re-download the script when a newer version is available. After installation run `source ~/.zshrc` or open a new shell and invoke `gifify`. +[MIT](LICENSE) diff --git a/VERSION b/VERSION index 3eefcb9..9084fa2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.0 +1.1.0 diff --git a/gifify.sh b/gifify.sh index 81065e3..efc83fe 100755 --- a/gifify.sh +++ b/gifify.sh @@ -1,38 +1,193 @@ -GIFIFY_VERSION="1.0.0" +#!/usr/bin/env bash +# gifify — convert videos to optimized GIFs using ffmpeg's two-pass palette technique +# https://github.com/pidoshva/gifify -gifify() { - if ! command -v ffmpeg &> /dev/null; then - echo "Error: ffmpeg is not installed! Installing ffmpeg..." - brew install ffmpeg - fi +GIFIFY_VERSION="1.1.0" + +_gifify_ensure_ffmpeg() { + command -v ffmpeg >/dev/null 2>&1 && return 0 + + echo "Error: ffmpeg is not installed." >&2 + echo "" >&2 + + local pkg_cmd="" + if command -v brew >/dev/null 2>&1; then + pkg_cmd="brew install ffmpeg" + elif command -v apt-get >/dev/null 2>&1; then + pkg_cmd="sudo apt-get update && sudo apt-get install -y ffmpeg" + elif command -v dnf >/dev/null 2>&1; then + pkg_cmd="sudo dnf install -y ffmpeg" + elif command -v pacman >/dev/null 2>&1; then + pkg_cmd="sudo pacman -S --noconfirm ffmpeg" + fi + + if [ -n "$pkg_cmd" ]; then + printf "Install ffmpeg now with: %s ? [y/N] " "$pkg_cmd" >&2 + read -r answer + if [ "$answer" = "y" ] || [ "$answer" = "Y" ]; then + eval "$pkg_cmd" || { echo "ffmpeg installation failed." >&2; return 1; } + return 0 + fi + fi - if [[ -z "$1" ]]; then - echo "Usage: gifify [--1080p|--720p|--480p]" + echo "Please install ffmpeg manually and try again." >&2 return 1 - fi +} + +gifify() { + # ── Help & version ────────────────────────────────────────────── + local _gifify_usage + _gifify_usage="Usage: gifify [options] + +Options: + -o, --output Output file path (default: ~/Desktop/gifified/.gif) + --fps Frames per second (default: 15) + --1080p Scale to 1920px width (default) + --720p Scale to 1280px width + --480p Scale to 854px width + --360p Scale to 640px width + -h, --help Show this help message + -v, --version Show version" - local input="$1" - local filename=$(basename "$input") - local name="${filename%.*}" - local output_dir="$HOME/Desktop/gifified" - mkdir -p "$output_dir" - local output="${output_dir}/${name}.gif" + # ── Parse arguments ───────────────────────────────────────────── + local input="" output="" fps=15 width=1920 - local width="1920" # Default: 1080p width + while [ $# -gt 0 ]; do + case "$1" in + -h|--help) + echo "$_gifify_usage" + return 0 + ;; + -v|--version) + echo "gifify $GIFIFY_VERSION" + return 0 + ;; + -o|--output) + if [ -z "${2:-}" ]; then + echo "Error: --output requires a path argument." >&2 + return 1 + fi + output="$2" + shift 2 + ;; + --fps) + if [ -z "${2:-}" ]; then + echo "Error: --fps requires a numeric argument." >&2 + return 1 + fi + fps="$2" + shift 2 + ;; + --1080p) width=1920; shift ;; + --720p) width=1280; shift ;; + --480p) width=854; shift ;; + --360p) width=640; shift ;; + -*) + echo "Error: unknown option '$1'" >&2 + echo "$_gifify_usage" >&2 + return 1 + ;; + *) + if [ -z "$input" ]; then + input="$1" + else + echo "Error: unexpected argument '$1'" >&2 + return 1 + fi + shift + ;; + esac + done - if [[ "$*" == *"--720p"* ]]; then - width="1280" - elif [[ "$*" == *"--480p"* ]]; then - width="854" - fi + # ── Validate input ────────────────────────────────────────────── + if [ -z "$input" ]; then + echo "$_gifify_usage" >&2 + return 1 + fi - echo "Generating GIF from $input with width $width..." + if [ ! -e "$input" ]; then + echo "Error: file not found: $input" >&2 + return 1 + fi - # Generate a color palette for accurate colors - ffmpeg -y -i "$input" -vf "fps=15,scale=${width}:-1:flags=lanczos,palettegen" /tmp/palette.png + if [ ! -f "$input" ]; then + echo "Error: not a regular file: $input" >&2 + return 1 + fi - # Create the GIF using the generated palette - ffmpeg -y -i "$input" -i /tmp/palette.png -filter_complex "fps=15,scale=${width}:-1:flags=lanczos[x];[x][1:v]paletteuse" "$output" + # ── Check ffmpeg ──────────────────────────────────────────────── + _gifify_ensure_ffmpeg || return 1 - echo "GIF created at: $output" + # ── Resolve output path ───────────────────────────────────────── + if [ -z "$output" ]; then + local filename name output_dir + filename="$(basename "$input")" + name="${filename%.*}" + output_dir="$HOME/Desktop/gifified" + mkdir -p "$output_dir" + output="${output_dir}/${name}.gif" + fi + + # ── Safe temp file with trap ──────────────────────────────────── + local palette + palette="$(mktemp "${TMPDIR:-/tmp}/gifify_palette_XXXXXX.png")" + + # Save any existing EXIT trap, set ours, restore after + local _old_trap + _old_trap="$(trap -p EXIT)" + trap 'rm -f "$palette"' EXIT + + # ── Pass 1: generate palette ──────────────────────────────────── + echo "[1/2] Generating palette..." + if ! ffmpeg -y -i "$input" \ + -vf "fps=${fps},scale=${width}:-1:flags=lanczos,palettegen" \ + "$palette" \ + -loglevel error -stats; then + echo "Error: palette generation failed." >&2 + rm -f "$palette" + eval "$_old_trap" + return 1 + fi + + # ── Pass 2: create GIF ────────────────────────────────────────── + echo "[2/2] Creating GIF..." + if ! ffmpeg -y -i "$input" -i "$palette" \ + -filter_complex "fps=${fps},scale=${width}:-1:flags=lanczos[x];[x][1:v]paletteuse" \ + "$output" \ + -loglevel error -stats; then + echo "Error: GIF creation failed." >&2 + rm -f "$palette" + eval "$_old_trap" + return 1 + fi + + # ── Cleanup & report ──────────────────────────────────────────── + rm -f "$palette" + eval "$_old_trap" + + local size + if stat --version >/dev/null 2>&1; then + # GNU stat + size="$(stat -c%s "$output" 2>/dev/null)" + else + # BSD stat (macOS) + size="$(stat -f%z "$output" 2>/dev/null)" + fi + + if [ -n "$size" ]; then + local human_size + if [ "$size" -ge 1048576 ]; then + human_size="$(awk "BEGIN {printf \"%.1f MB\", $size/1048576}")" + else + human_size="$(awk "BEGIN {printf \"%.0f KB\", $size/1024}")" + fi + echo "Done! $output ($human_size)" + else + echo "Done! $output" + fi } + +# Allow running as a script: ./gifify.sh [args] +if [ "${BASH_SOURCE[0]}" = "$0" ] 2>/dev/null; then + gifify "$@" +fi diff --git a/install.sh b/install.sh index c167908..df4fe65 100755 --- a/install.sh +++ b/install.sh @@ -1,69 +1,129 @@ -#!/bin/bash -set -e +#!/usr/bin/env bash +set -euo pipefail -echo "Installing gifify..." +GIFIFY_DIR="$HOME/.gifify" +REPO_URL="https://raw.githubusercontent.com/pidoshva/gifify/main" + +# ── Helpers ───────────────────────────────────────────────────────── + +info() { echo " $*"; } +error() { echo "Error: $*" >&2; } + +detect_rc_file() { + case "$(basename "${SHELL:-/bin/bash}")" in + zsh) echo "$HOME/.zshrc" ;; + *) echo "$HOME/.bashrc" ;; + esac +} -# Detect package manager for ffmpeg installation install_ffmpeg() { - if command -v brew >/dev/null; then - echo "Installing ffmpeg with Homebrew..." + if command -v brew >/dev/null 2>&1; then + info "Installing ffmpeg with Homebrew..." brew install ffmpeg - elif command -v apt-get >/dev/null; then - echo "Installing ffmpeg with apt-get..." + elif command -v apt-get >/dev/null 2>&1; then + info "Installing ffmpeg with apt-get..." sudo apt-get update && sudo apt-get install -y ffmpeg + elif command -v dnf >/dev/null 2>&1; then + info "Installing ffmpeg with dnf..." + sudo dnf install -y ffmpeg + elif command -v pacman >/dev/null 2>&1; then + info "Installing ffmpeg with pacman..." + sudo pacman -S --noconfirm ffmpeg else - echo "No supported package manager found. Please install ffmpeg manually." >&2 + error "No supported package manager found. Please install ffmpeg manually." return 1 fi } +# ── Uninstall ─────────────────────────────────────────────────────── -if ! command -v ffmpeg >/dev/null; then - echo "ffmpeg not found." - install_ffmpeg || exit 1 -fi +do_uninstall() { + echo "Uninstalling gifify..." -GIFIFY_DIR="$HOME/.gifify" -mkdir -p "$GIFIFY_DIR" + if [ -d "$GIFIFY_DIR" ]; then + rm -rf "$GIFIFY_DIR" + info "Removed $GIFIFY_DIR" + fi -# URLs for downloading the script and version -GIFIFY_URL="https://raw.githubusercontent.com/pidoshva/gifify/main/gifify.sh" -VERSION_URL="https://raw.githubusercontent.com/pidoshva/gifify/main/VERSION" + local rc_file + rc_file="$(detect_rc_file)" -# Check remote version -REMOTE_VERSION=$(curl -fsSL "$VERSION_URL") -LOCAL_VERSION="none" -[ -f "$GIFIFY_DIR/VERSION" ] && LOCAL_VERSION=$(cat "$GIFIFY_DIR/VERSION") + # Also clean the other rc file in case they switched shells + local rc_files=("$rc_file") + [ "$rc_file" != "$HOME/.bashrc" ] && rc_files+=("$HOME/.bashrc") + [ "$rc_file" != "$HOME/.zshrc" ] && rc_files+=("$HOME/.zshrc") -if [ "$REMOTE_VERSION" != "$LOCAL_VERSION" ]; then - echo "Installing gifify version $REMOTE_VERSION..." - curl -fsSL "$GIFIFY_URL" -o "$GIFIFY_DIR/gifify.sh" - echo "$REMOTE_VERSION" > "$GIFIFY_DIR/VERSION" -else - echo "gifify is up to date (version $LOCAL_VERSION)" -fi + for rc in "${rc_files[@]}"; do + if [ -f "$rc" ] && grep -q 'gifify' "$rc" 2>/dev/null; then + # Remove the comment line and the source line + sed -i.bak '/# Load gifify/d;/\.gifify\/gifify\.sh/d' "$rc" + rm -f "${rc}.bak" + info "Cleaned $rc" + fi + done + echo "gifify uninstalled." +} -if ! command -v ffmpeg >/dev/null; then - echo "ffmpeg not found." - install_ffmpeg || exit 1 -fi +# ── Install ───────────────────────────────────────────────────────── -GIFIFY_DIR="$HOME/.gifify" -mkdir -p "$GIFIFY_DIR" -cp "gifify.sh" "$GIFIFY_DIR/gifify.sh" - -# Ensure gifify function is loaded in zsh -ZSHRC="$HOME/.zshrc" -SOURCE_LINE="source \"$HOME/.gifify/gifify.sh\"" -if ! grep -Fq 'gifify.sh' "$ZSHRC" 2>/dev/null; then - echo "Adding gifify function to $ZSHRC..." - echo -e "\n# Load gifify function\n$SOURCE_LINE" >> "$ZSHRC" -fi - -# Source the function for current session if running zsh -if [ -n "$ZSH_VERSION" ]; then - source "$ZSHRC" -fi - -echo "gifify installed! Open a new terminal or run 'source $ZSHRC' to start using it." +do_install() { + echo "Installing gifify..." + + # 1. Ensure ffmpeg is available + if ! command -v ffmpeg >/dev/null 2>&1; then + info "ffmpeg not found." + install_ffmpeg || exit 1 + fi + + # 2. Create install directory + mkdir -p "$GIFIFY_DIR" + + # 3. Copy or download gifify.sh + local script_dir + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + + if [ -f "$script_dir/gifify.sh" ]; then + # Local clone install + cp "$script_dir/gifify.sh" "$GIFIFY_DIR/gifify.sh" + info "Copied gifify.sh from local repo" + # Copy VERSION if present + if [ -f "$script_dir/VERSION" ]; then + cp "$script_dir/VERSION" "$GIFIFY_DIR/VERSION" + fi + else + # Remote install (curl pipe) + info "Downloading gifify.sh..." + curl -fsSL "$REPO_URL/gifify.sh" -o "$GIFIFY_DIR/gifify.sh" + curl -fsSL "$REPO_URL/VERSION" -o "$GIFIFY_DIR/VERSION" + fi + + chmod +x "$GIFIFY_DIR/gifify.sh" + + # 4. Add source line to shell RC file + local rc_file source_line + rc_file="$(detect_rc_file)" + source_line="source \"$GIFIFY_DIR/gifify.sh\"" + + if ! grep -Fq 'gifify.sh' "$rc_file" 2>/dev/null; then + info "Adding gifify to $rc_file..." + printf '\n# Load gifify\n%s\n' "$source_line" >> "$rc_file" + else + info "gifify already configured in $rc_file" + fi + + echo "" + echo "gifify installed! Run 'source $rc_file' or open a new terminal to start using it." + echo "Usage: gifify [--720p|--480p|--360p]" +} + +# ── Main ──────────────────────────────────────────────────────────── + +main() { + case "${1:-}" in + --uninstall) do_uninstall ;; + *) do_install ;; + esac +} + +main "$@"