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-stylejarvis-judice-ninke- Larger kernel, smoother gradientsstucki- Modified JJN, good balanceburkes- Simplified Stucki, fastersierra,sierra-lite,two-row-sierra- Sierra variantssimple-2d- Simple 2D error diffusionfalse-floyd-steinberg- Simplified Floyd-Steinberg
Ordered (parallel, faster):
bayer- Bayer matrix (configurable size)clustered-dot-*- Various clustered dot patterns
Other:
random-noise- Random noise ditheringthreshold- 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..."
}
pixelscontains 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
}
framesis an array of RLE-compressed pixel data (same encoding as images)frame-durationcontrols 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
¶
There is no documentation for this package.