particle-img

command module
v0.0.0-...-f0620f4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 24, 2026 License: MPL-2.0 Imports: 1 Imported by: 0

README

particle-img

Convert images to the Particle Image format (.pimg) and animated GIFs to the Particle Reel format (.preel) for the Playdate Constellation Browser.

Installation

go install github.com/jeffory/particle-img@latest

Or build from source:

git clone https://github.com/jeffory/particle-img
cd particle-img
go build -o particle-img .

CLI Usage

particle-img [flags] <input-file>
Basic Examples
# Convert with default settings (Floyd-Steinberg dithering)
particle-img photo.png

# Use Atkinson dithering with preview
particle-img -d atkinson -p photo.jpg

# Use Bayer dithering with custom matrix size
particle-img -d bayer --bayer-size 4 photo.png

# Crop to exact dimensions (400x260) instead of fit
particle-img --crop photo.jpg

# Crop with top anchor (keep top of image)
particle-img --crop --anchor top photo.jpg

# Fine-tune crop position with offset
particle-img --crop --offset-x 20 --offset-y -10 photo.jpg

# Custom output path
particle-img -o output.pimg input.png

# Brighten a dark image before conversion
particle-img -b 30 dark-photo.jpg

# Increase contrast for better dithering
particle-img -c 20 low-contrast.png
Flags
Flag Short Default Description
--output -o <input>.pimg Output file path
--dither -d floyd-steinberg Dithering algorithm
--strength -s 100 Dithering strength (0-100)
--threshold -t 50 B/W threshold for 'threshold' dither (0-100)
--serpentine false Enable serpentine mode for error diffusion
--bayer-size 8 Bayer matrix size: 2, 4, 8, or 16
--preview -p false Generate PNG preview alongside .pimg
--crop false Fill dimensions and crop (vs fit within)
--anchor center Crop anchor: center, top, bottom, left, right
--offset-x 0 Horizontal crop offset (positive = right, negative = left)
--offset-y 0 Vertical crop offset (positive = down, negative = up)
--width 400 Target width
--height 260 Target height
--brightness -b 0 Brightness adjustment (-100 to +100)
--contrast -c 0 Contrast adjustment (-100 to +100)
--list-dithers List available dithering algorithms
Dithering Algorithms

Error Diffusion (sequential, higher quality):

  • floyd-steinberg - Classic error diffusion (default)
  • atkinson - Good for high contrast, Mac-style
  • jarvis-judice-ninke - Larger kernel, smoother gradients
  • stucki - Modified JJN, good balance
  • burkes - Simplified Stucki, faster
  • sierra, sierra-lite, two-row-sierra - Sierra variants
  • simple-2d - Simple 2D error diffusion
  • false-floyd-steinberg - Simplified Floyd-Steinberg

Ordered (parallel, faster):

  • bayer - Bayer matrix (configurable size)
  • clustered-dot-* - Various clustered dot patterns

Other:

  • random-noise - Random noise dithering
  • threshold - Simple threshold (no dithering)

Reel Command (Animated GIFs)

Convert animated GIFs to the Particle reel format for animations.

particle-img reel [flags] <input.gif>
Reel Examples
# Convert with default settings
particle-img reel animation.gif

# Use Atkinson dithering with preview
particle-img reel -d atkinson -p animation.gif

# Set target FPS (overrides GIF timing)
particle-img reel --fps 15 animation.gif

# Limit frames and skip every other frame
particle-img reel --max-frames 60 --skip 2 large-animation.gif

# Custom output path
particle-img reel -o output.preel input.gif
Reel Flags
Flag Short Default Description
--output -o <input>.preel Output file path
--dither -d floyd-steinberg Dithering algorithm
--strength -s 100 Dithering strength (0-100)
--threshold -t 50 B/W threshold for 'threshold' dither (0-100)
--serpentine false Enable serpentine mode for error diffusion
--bayer-size 8 Bayer matrix size: 2, 4, 8, or 16
--preview -p false Generate preview GIF alongside .preel
--max-frames 0 Maximum frames (0 = up to 90)
--skip 1 Take every Nth frame (1 = all frames)
--fps 0 Target FPS (0 = use GIF timing)
--width 0 Target width (0 = auto, max 200)
--height 0 Target height (0 = auto, max 120)
--brightness -b 0 Brightness adjustment (-100 to +100)
--contrast -c 0 Contrast adjustment (-100 to +100)
Reel Constraints
  • Maximum dimensions: 200x120 pixels
  • Maximum frames: 90
  • Frame duration range: 1-1800 (at 30 FPS base)

View Command

View .pimg images or play .preel animations.

particle-img view [flags] <file.pimg|file.preel>
View Examples
# View image in system viewer
particle-img view image.pimg

# View with 2x scaling
particle-img view --scale 2 image.pimg

# Display in terminal (requires sixel/kitty/iTerm support)
particle-img view --terminal image.pimg

# Play animation scaled 3x in terminal
particle-img view -t -s 3 animation.preel
View Flags
Flag Short Default Description
--terminal -t false Display in terminal instead of system viewer
--scale -s 1 Scale factor for display (1-10)

Note: Terminal display requires a terminal emulator with sixel, kitty, or iTerm graphics protocol support. Animation playback loops until interrupted with Ctrl+C.

Library Usage

The packages are exported under pkg/ for use as a library.

Quick Start
package main

import (
    "fmt"
    "image"
    _ "image/png"
    "os"

    "github.com/jeffory/particle-img/pkg/converter"
)

func main() {
    // Load an image
    img, _ := converter.LoadImage("input.png")

    // Convert to Particle format (no file I/O)
    result, _ := converter.ConvertImage(img, converter.ImageOptions{
        Dither: converter.DitherConfig{
            Algorithm: "floyd-steinberg",
            Strength:  1.0,
        },
        Resize: converter.ResizeConfig{
            Mode:         converter.ResizeModeFit,
            TargetWidth:  400,
            TargetHeight: 260,
        },
    })

    // Get JSON as string
    jsonStr, _ := result.String()
    fmt.Println(jsonStr)

    // Or get as bytes
    jsonBytes, _ := result.JSON()

    // Or write to file
    result.WriteToFile("output.pimg")
}
File-based Conversion

For simple file-to-file conversion:

result, _ := converter.Convert(converter.Options{
    InputPath:  "input.png",
    OutputPath: "output.pimg",
    Preview:    true,  // Also generate preview PNG
    Dither:     converter.DitherConfig{Algorithm: "atkinson"},
    Resize:     converter.ResizeConfig{Mode: converter.ResizeModeFit},
})
Packages
pkg/pimg - Particle Image Format

Encode images to the Particle Image JSON format.

import "github.com/jeffory/particle-img/pkg/pimg"

// Encode a black/white image to Particle format
encoded, err := pimg.Encode(img)

// Get JSON output
jsonBytes, _ := encoded.ToJSON()          // Compact
jsonBytes, _ := encoded.ToJSONIndented()  // Pretty-printed

// Access fields
fmt.Println(encoded.Width, encoded.Height)
fmt.Println(encoded.Data)  // RLE-compressed pixel data
pkg/converter - Image Processing

Resize, dither, and convert images.

import "github.com/jeffory/particle-img/pkg/converter"

// Load an image
img, err := converter.LoadImage("photo.png")

// Full conversion (no file I/O)
result, err := converter.ConvertImage(img, converter.ImageOptions{
    Dither: converter.DitherConfig{Algorithm: "floyd-steinberg"},
    Resize: converter.ResizeConfig{Mode: converter.ResizeModeFit},
})

// Get output
jsonStr, _ := result.String()       // JSON string
jsonBytes, _ := result.JSON()       // JSON bytes (compact)
jsonBytes, _ := result.JSONIndented() // JSON bytes (pretty)
result.WriteToFile("output.pimg")   // Write to file

// Access result fields
result.ParticleImage  // *pimg.ParticleImage
result.DitheredImage  // image.Image (the dithered result)
result.OutputWidth    // Final width
result.OutputHeight   // Final height

// Resize functions
img := converter.FitWithinBounds(src, 400, 260)      // Fit, maintain aspect
img := converter.FillAndCrop(src, 400, 260, converter.CropAnchorCenter, 0, 0)  // Fill and crop
img := converter.ResizeWithConfig(src, converter.ResizeConfig{
    Mode:         converter.ResizeModeFill,
    Anchor:       converter.CropAnchorTop,
    TargetWidth:  400,
    TargetHeight: 260,
    OffsetX:      20,   // Fine-tune crop position
    OffsetY:      -10,
})

// Dithering
dithered, err := converter.ApplyDither(img, converter.DitherConfig{
    Algorithm:  "floyd-steinberg",
    Strength:   1.0,        // 0.0-1.0
    Threshold:  0.5,        // For threshold algorithm
    Serpentine: true,       // Serpentine scanning
    BayerSize:  8,          // For bayer algorithm
})

// List available algorithms
names := converter.AlgorithmNames()
info := converter.SupportedAlgorithms()

// Convert animated GIF to reel
reelResult, err := converter.ConvertGIFToReel("animation.gif", converter.ReelConfig{
    Dither: converter.DitherConfig{Algorithm: "floyd-steinberg"},
    MaxFrames:  60,    // Limit to 60 frames
    SkipFrames: 2,     // Take every 2nd frame
    TargetFPS:  15,    // Override GIF timing
})

// Access reel result
jsonStr, _ := reelResult.String()
reelResult.Reel           // *preel.Reel
reelResult.DitheredFrames // []image.Image
reelResult.OutputFrames   // Number of frames in output
pkg/preel - Particle Reel Format

Encode animated images to the Particle Reel JSON format.

import "github.com/jeffory/particle-img/pkg/preel"

// Load an animated GIF
gifData, err := preel.LoadGIF("animation.gif")
// gifData.Frames  - []image.Image
// gifData.Delays  - []int (in 100ths of a second)
// gifData.Width, gifData.Height

// Encode frames to reel format
reel, err := preel.Encode(ditheredFrames, frameDuration)

// Get JSON output
jsonBytes, _ := reel.ToJSON()          // Compact
jsonBytes, _ := reel.ToJSONIndented()  // Pretty-printed

// Access fields
fmt.Println(reel.Width, reel.Height)
fmt.Println(len(reel.Frames))        // Number of frames
fmt.Println(reel.FrameDuration)      // Playback speed

// Calculate frame duration from GIF delays
frameDuration := preel.CalculateFrameDuration(gifData.Delays)

// Or from target FPS
frameDuration := preel.FrameDurationFromFPS(15)  // 15 FPS
pkg/compress - RLE Compression

Run-length encoding for pixel data.

import "github.com/jeffory/particle-img/pkg/compress"

// Encode binary string (1s and 0s) to RLE
// 1s -> A-Y (A=1, B=2, ... Y=25)
// 0s -> a-y (a=1, b=2, ... y=25)
encoded := compress.Encode("111100001111")  // "DdD"

// Decode back to binary
decoded := compress.Decode("DdD")  // "111100001111"

Output Formats

Image Format (.pimg)

The .pimg format is JSON:

{
  "type": "image",
  "width": 400,
  "height": 260,
  "pixels": "YYY... AaBa... CdE..."
}
  • pixels contains RLE-compressed pixel data
  • Rows are separated by spaces
  • A-Y = runs of 1-25 black pixels (1s)
  • a-y = runs of 1-25 white pixels (0s)
Reel Format (.preel)

The .preel format is JSON for animations:

{
  "type": "reel",
  "width": 200,
  "height": 120,
  "frames": ["frame1_pixels", "frame2_pixels", "..."],
  "frame-duration": 3
}
  • frames is an array of RLE-compressed pixel data (same encoding as images)
  • frame-duration controls playback speed (1 = 30 FPS, 30 = 1 FPS)
  • Maximum 90 frames, 200x120 pixels

Supported Input Formats

  • PNG
  • JPEG
  • GIF (static and animated)
  • BMP
  • WebP

AI Disclosure

Portions of this codebase were developed with assistance from Claude, an AI assistant by Anthropic. All AI-generated code has been reviewed and tested.

License

This project is licensed under the Mozilla Public License 2.0 - see the LICENSE file for details.

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
pkg
viewer
Package viewer provides image and animation display functionality.
Package viewer provides image and animation display functionality.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL