Automatically upload images from your photo collection to your TRMNL e-ink display with proper dithering for beautiful grayscale rendering.
- 📸 Automatic Image Processing - Scales and converts photos to e-ink format
- 🎨 Floyd-Steinberg Dithering - Professional halftone effect for smooth gradients
- 🖼️ 2-bit Grayscale - 4 shades of gray for better quality than pure black & white
- ✨ Gamma Correction - Brightens midtones for better e-ink visibility
- 🖼️ Frame Borders - Optional decorative borders (line or rounded corners)
- 🎨 Color Output Mode - Full RGB for Pimoroni Inky or similar color e-ink displays
- 🎲 Multiple Selection Modes - Random, sequential, shuffle, newest, or oldest
- 🔄 Auto-Rotation - Respects EXIF orientation data
- 🖼️ Flexible Layouts - Auto, landscape, or portrait modes
- 🎯 Orientation Filtering - Show only landscape or portrait photos (with smart caching)
- 🎨 Border Styles - White, black, or blurred borders
- 📏 Adjustable Margins - Add spacing around images for framed picture look
- 📁 Subfolder Support - Organize photos in folders and subfolders
- 🏷️ Optional Labels - Show filename or path on images
- 🐳 Docker Ready - Easy deployment with docker-compose
- 💾 State Management - Remembers position for sequential/shuffle modes
- 🔍 Debug Mode - Saves processed images for inspection
- 🧪 Dry Run - Test without uploading
- 🔔 Version Checking - Notifies when updates are available
- Log into TRMNL
- Go to Plugins > Webhook Image
- Click "Add to my plugins"
- Copy your webhook URL
# Clone or download this repository
cd trmnl-image-webhook
# Copy example config
cp .env.example .env
# Edit with your settings
nano .envMinimum required config:
WEBHOOK_URL=https://usetrmnl.com/api/plugin_settings/YOUR-UUID/image
IMAGES_PATH=/path/to/your/photosdocker-compose up -dThat's it! Your TRMNL will start showing photos from your collection.
| Variable | Description | Example |
|---|---|---|
WEBHOOK_URL |
Your TRMNL webhook URL | https://usetrmnl.com/api/... |
IMAGES_PATH |
Path to your photos | ./images or /home/user/Photos |
| Variable | Default | Description |
|---|---|---|
BIT_DEPTH |
2 |
Bit depth: 1 = black/white, 2 = 4 grays (recommended) |
DISPLAY_WIDTH |
800 |
Display width (OG: 800) |
DISPLAY_HEIGHT |
480 |
Display height (OG: 480) |
LAYOUT |
auto |
Orientation: auto/landscape/portrait |
ORIENTATION_FILTER |
any |
Filter images: any/landscape/portrait |
BORDER_STYLE |
white |
Border style: white/black/blur |
MARGIN |
0 |
Margin in pixels (0-100) for framed look |
GAMMA |
1.5 |
Gamma correction (1.0-2.0, brightens midtones for e-ink) |
FRAME_BORDER |
none |
Frame border: none/line/rounded (only with MARGIN > 0) |
FRAME_BORDER_WIDTH |
2 |
Frame border width in pixels (1-10) |
OUTPUT_MODE |
grayscale |
Output: grayscale (TRMNL) or color (Pimoroni Inky) |
INTERVAL_MINUTES |
60 |
Minutes between uploads |
SELECTION_MODE |
random |
How to pick images (see below) |
INCLUDE_SUBFOLDERS |
true |
Include images from subdirectories |
USE_DITHERING |
true |
Apply Floyd-Steinberg dithering |
IMAGE_LABEL |
none |
Show label (none/filename/path) |
DRY_RUN |
false |
Test mode - don't upload |
- random - Pick any image randomly
- sequential - Go through images A-Z, remembers position
- shuffle - Random order, each image once before reshuffling
- newest - Always show most recently modified image
- oldest - Show oldest image first
Add text overlay to images:
- none - No label (default)
- filename - Show just the filename
- path - Show relative path from images directory
Example with label:
IMAGE_LABEL=pathBrightens midtones for better e-ink visibility. E-ink displays tend to look darker than regular screens, so gamma correction helps.
GAMMA=1.5 # Recommended for e-ink (default)
GAMMA=1.0 # No correction (original brightness)
GAMMA=1.8 # More aggressive brighteningAdd decorative borders around images for a framed picture look. Requires MARGIN > 0.
MARGIN=40 # Space around image
FRAME_BORDER=line # Style: none/line/rounded
FRAME_BORDER_WIDTH=3 # Border thickness (1-10 pixels)
BORDER_STYLE=white # Background: white/blackExamples:
- Gallery style:
MARGIN=60,FRAME_BORDER=line,BORDER_STYLE=black - Modern:
MARGIN=30,FRAME_BORDER=rounded,BORDER_STYLE=white - Classic:
MARGIN=50,FRAME_BORDER=line,FRAME_BORDER_WIDTH=5
For Pimoroni Inky or other color e-ink displays. Outputs full RGB instead of dithered grayscale.
OUTPUT_MODE=color # Enable color mode
WEBHOOK_URL=http://192.168.1.x:5000/display # Your local endpointNote: Color mode produces larger files (200-500KB) and is not compatible with TRMNL's cloud service. Use for local Pimoroni Inky setups only.
Control how images are scaled to the display:
Contain (default):
IMAGE_FIT=contain- Fits entire image on screen
- Maintains aspect ratio
- May show borders if image ratio doesn't match display
- Best for preserving full image
Fill:
IMAGE_FIT=fill- Fills entire display
- Maintains aspect ratio
- May crop edges of image
- Best for edge-to-edge display, no borders
- Great for wallpaper-style images
Example - Full Screen Photos:
IMAGE_FIT=fill
MARGIN=0
BORDER_STYLE=white- Scaling - Resized to fit display (800x480 or 1280x800)
- Grayscale - Converted to grayscale
- Dithering - Floyd-Steinberg dithering applied for smooth gradients
- 1-bit Conversion - Pure black and white (2 colors)
- PNG Export - Optimized 1-bit PNG (~20-40KB)
TRMNL displays are 1-bit (pure black and white). Dithering creates the illusion of grayscale by using patterns of black and white dots - like newspaper photos. This makes photos look much better than simple thresholding.
With dithering:
- Smooth gradients in sky, skin tones, etc.
- Details visible in shadows and highlights
- Professional halftone appearance
Without dithering:
- Harsh black/white contrast
- Loss of detail
- Posterized look
# .env
WEBHOOK_URL=https://usetrmnl.com/api/plugin_settings/abc-123/image
IMAGES_PATH=/home/user/Photos
INTERVAL_MINUTES=60
SELECTION_MODE=random
INCLUDE_SUBFOLDERS=true
USE_DITHERING=trueSELECTION_MODE=sequential
INTERVAL_MINUTES=120
IMAGE_LABEL=filenameSELECTION_MODE=shuffle
INTERVAL_MINUTES=30
INCLUDE_SUBFOLDERS=trueSELECTION_MODE=newest
INTERVAL_MINUTES=15
IMAGE_LABEL=pathMARGIN=40
BORDER_STYLE=white
FRAME_BORDER=line
FRAME_BORDER_WIDTH=3
GAMMA=1.5MARGIN=60
BORDER_STYLE=black
FRAME_BORDER=rounded
FRAME_BORDER_WIDTH=2OUTPUT_MODE=color
DISPLAY_WIDTH=800
DISPLAY_HEIGHT=480
GAMMA=1.5
MARGIN=30
FRAME_BORDER=line
WEBHOOK_URL=http://192.168.1.x:5000/display # Your local Pi endpoint# Start
docker-compose up -d
# View logs
docker-compose logs -f trmnl-image-webhook
# Stop
docker-compose down
# Restart after config changes
docker-compose restart- Enable Docker in Package Center
- Upload project folder to your NAS
- Edit
.envwith your settings - SSH into NAS:
cd /volume1/docker/trmnl-image-webhook
docker-compose up -d# Install Docker
curl -fsSL https://get.docker.com | sh
# Clone and configure
git clone <repo-url>
cd trmnl-image-webhook
cp .env.example .env
nano .env
# Run
docker-compose up -ddocker-compose logs -fLook for:
Found X images
Converting to grayscale
Converting to 1-bit with Floyd-Steinberg dithering
Final: 800x480 1-bit PNG, 25.3KB
✓ Successfully uploaded image.jpg
Response: 200
Every upload saves two files to ./data/:
last_original.jpg- Original unprocessed photolast_processed.png- Final 1-bit dithered PNG sent to TRMNL
Compare these to see exactly what's being displayed.
Test without uploading:
DRY_RUN=true
docker-compose restartNo images found
- Check
IMAGES_PATHis correct - Set
INCLUDE_SUBFOLDERS=trueif images are in subdirectories - Verify image formats (PNG, JPG, JPEG, BMP, GIF supported)
Images not displaying on TRMNL
- Check device WiFi connection
- Verify webhook URL is correct
- Try "Force Refresh" in TRMNL plugin settings
- Check
data/last_processed.pnglooks correct
Rate limited (429 error)
- TRMNL allows max 12 uploads per hour
- Increase
INTERVAL_MINUTESto 60 or higher
Input: PNG, JPEG, JPG, BMP, GIF
Output: 1-bit or 2-bit grayscale PNG (default: 2-bit)
2-bit (Default - Recommended):
- 4 shades of gray (0, 85, 170, 255)
- Smoother gradients and better photo quality
- Smaller file sizes (~5-10KB)
- Supported on TRMNL OG firmware v1.7.2+
1-bit (Classic):
- Pure black and white (2 colors)
- Sharper, higher contrast
- Slightly larger files (~15-30KB)
- Maximum compatibility
Both modes use Floyd-Steinberg dithering for professional halftone effects.
- Input: Any size (auto-scaled)
- Output: ~15-40KB (1-bit PNG)
- Limit: 5MB (rarely reached)
TRMNL OG:
DISPLAY_WIDTH=800
DISPLAY_HEIGHT=480For sequential and shuffle modes, position is saved in ./data/state.json:
{
"last_image": "vacation/photo.jpg",
"current_index": 42,
"last_upload": "2024-12-31T16:20:57"
}- Memory: ~50MB
- CPU: Minimal (only during processing)
- Network: ~20-40KB per upload
- Storage: State file <1KB
- Docker & Docker Compose
- Network access to TRMNL API
- Directory of images (local or mounted)
MIT License - see LICENSE file
Issues and pull requests welcome!
For issues with:
- This tool: Open a GitHub issue
- TRMNL device/service: Contact TRMNL support
- Docker/deployment: Check Docker logs first
Developed for the TRMNL community. Inspired by TRMNL's own image processing code.
