Repository: kevin-cantwell/dotmatrix
Branch: main
Commit: b9562229819b
Files: 17
Total size: 61.4 KB
Directory structure:
gitextract_fwtecarx/
├── .github/
│ └── workflows/
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── braille.go
├── doc.go
├── dotmatrix_suite_test.go
├── ffmpeg_check.go
├── gif.go
├── go.mod
├── go.sum
├── image.go
├── image_test.go
├── mise.toml
├── mp4.go
└── webcam.go
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
jobs:
release:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04
artifact: dotmatrix-linux-amd64
- os: macos-15
artifact: dotmatrix-darwin-arm64
- os: macos-15-intel
artifact: dotmatrix-darwin-amd64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install build dependencies (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y nasm yasm pkg-config
- name: Install build dependencies (macOS)
if: runner.os == 'macOS'
run: |
brew install nasm pkg-config
- name: Build FFmpeg 8.0.1 (minimal static)
run: |
curl -L https://ffmpeg.org/releases/ffmpeg-8.0.1.tar.xz | tar -xJ
cd ffmpeg-8.0.1
# Build minimal FFmpeg with only built-in codecs (no external libs)
# This ensures fully static binary with no runtime dependencies
./configure \
--enable-static \
--disable-shared \
--disable-programs \
--disable-doc \
--disable-network \
--disable-encoders \
--disable-muxers \
--disable-outdevs \
--disable-filters \
--enable-filter=scale \
--enable-filter=format \
--disable-xlib \
--disable-libxcb \
--disable-sdl2 \
--disable-lzma \
--disable-zlib \
--disable-bzlib \
--disable-iconv \
--disable-libxml2 \
--disable-securetransport \
--prefix=$HOME/ffmpeg
make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu)
make install
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Build with static FFmpeg
env:
CGO_ENABLED: 1
run: |
export PKG_CONFIG_PATH="$HOME/ffmpeg/lib/pkgconfig"
export CGO_LDFLAGS="$(pkg-config --static --libs libavdevice libavformat libavcodec libswscale libswresample libavutil)"
export CGO_CFLAGS="$(pkg-config --cflags libavdevice libavformat libavcodec libswscale libswresample libavutil)"
go build -ldflags "-X main.version=${{ steps.version.outputs.VERSION }}" -o dotmatrix ./cmd/dotmatrix
- name: Ad-hoc code sign (macOS)
if: runner.os == 'macOS'
run: codesign --sign - --force dotmatrix
- name: Create archive
run: |
tar -czvf ${{ matrix.artifact }}.tar.gz dotmatrix
- name: Upload release asset
uses: softprops/action-gh-release@v2
with:
files: ${{ matrix.artifact }}.tar.gz
update-homebrew:
needs: release
runs-on: ubuntu-24.04
steps:
- name: Get version from tag
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Download release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download v${{ steps.version.outputs.VERSION }} \
--repo kevin-cantwell/dotmatrix \
--pattern '*.tar.gz' \
--dir assets
- name: Compute SHA256 checksums
id: sha
run: |
echo "DARWIN_ARM64=$(sha256sum assets/dotmatrix-darwin-arm64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "DARWIN_AMD64=$(sha256sum assets/dotmatrix-darwin-amd64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
echo "LINUX_AMD64=$(sha256sum assets/dotmatrix-linux-amd64.tar.gz | awk '{print $1}')" >> $GITHUB_OUTPUT
- name: Checkout homebrew-tap
uses: actions/checkout@v4
with:
repository: kevin-cantwell/homebrew-tap
ssh-key: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
- name: Update formula
env:
VERSION: ${{ steps.version.outputs.VERSION }}
SHA_DARWIN_ARM64: ${{ steps.sha.outputs.DARWIN_ARM64 }}
SHA_DARWIN_AMD64: ${{ steps.sha.outputs.DARWIN_AMD64 }}
SHA_LINUX_AMD64: ${{ steps.sha.outputs.LINUX_AMD64 }}
run: |
cat > Formula/dotmatrix.rb <<EOF
class Dotmatrix < Formula
desc "Convert images and videos to Unicode braille art in the terminal"
homepage "https://github.com/kevin-cantwell/dotmatrix"
version "${VERSION}"
license "MIT"
on_macos do
on_arm do
url "https://github.com/kevin-cantwell/dotmatrix/releases/download/v#{version}/dotmatrix-darwin-arm64.tar.gz"
sha256 "${SHA_DARWIN_ARM64}"
end
on_intel do
url "https://github.com/kevin-cantwell/dotmatrix/releases/download/v#{version}/dotmatrix-darwin-amd64.tar.gz"
sha256 "${SHA_DARWIN_AMD64}"
end
end
on_linux do
on_intel do
url "https://github.com/kevin-cantwell/dotmatrix/releases/download/v#{version}/dotmatrix-linux-amd64.tar.gz"
sha256 "${SHA_LINUX_AMD64}"
end
end
def install
bin.install "dotmatrix"
end
test do
assert_match version.to_s, shell_output("#{bin}/dotmatrix --version")
end
end
EOF
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/dotmatrix.rb
git commit -m "Update dotmatrix to ${{ steps.version.outputs.VERSION }}"
git push
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on:
pull_request:
branches: [master, main]
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Install FFmpeg 7.x development libraries
run: |
sudo add-apt-repository -y ppa:ubuntuhandbook1/ffmpeg7
sudo apt-get update
sudo apt-get install -y libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavdevice-dev libavfilter-dev
- name: Build
run: CGO_ENABLED=1 go build ./...
- name: Test
run: CGO_ENABLED=1 go test ./... -v
================================================
FILE: .gitignore
================================================
_ignore/
.DS_Store
dotmatrix
================================================
FILE: LICENSE.md
================================================
The MIT License (MIT)
Copyright (c) 2016 Kevin Cantwell
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.
================================================
FILE: README.md
================================================
# Dotmatrix
[](https://pkg.go.dev/github.com/kevin-cantwell/dotmatrix)
Convert images to Unicode braille art for terminal display.
<table>
<tr>
<td><strong>Input</strong></td>
<td><strong>Output</strong></td>
</tr>
<tr>
<td><img src="https://cloud.githubusercontent.com/assets/307864/14945003/a928affe-0fd3-11e6-9725-ae6824be4317.png" alt="Input image" width="300"/></td>
<td><img src="https://cloud.githubusercontent.com/assets/307864/14945005/c9b0d53a-0fd3-11e6-9b06-841eb637a2a0.png" alt="Terminal output" width="300"/></td>
</tr>
</table>
## Features
- Encode JPEG, PNG, GIF, and BMP images as braille Unicode characters
- Animated GIF support with proper frame timing and disposal methods
- MP4 video playback with H.264 decoding
- Embedded subtitle rendering for MP4 files (mov_text format)
- Native webcam capture on macOS (AVFoundation)
- Image adjustments: gamma, brightness, contrast, sharpening
- Floyd-Steinberg dithering for grayscale preservation
- Automatic scaling to fit terminal dimensions
## Installation
### Homebrew (macOS/Linux)
```bash
brew install kevin-cantwell/tap/dotmatrix
```
### Pre-built Binaries
Download from [GitHub Releases](https://github.com/kevin-cantwell/dotmatrix/releases). Binaries are available for:
- Linux (amd64)
- macOS (arm64, amd64)
**macOS users:** The binaries are not signed with an Apple Developer account. After downloading, remove the quarantine attribute:
```bash
xattr -d com.apple.quarantine dotmatrix
```
### Install with Go
```bash
go install github.com/kevin-cantwell/dotmatrix/cmd/dotmatrix@latest
```
**macOS users:** The binary may be killed immediately due to an invalid code signature. If this happens, re-sign it:
```bash
codesign --force --sign - $(go env GOPATH)/bin/dotmatrix
```
### Building from Source
Building from source requires FFmpeg 8.x development libraries and CGO. Pre-built binaries have FFmpeg statically linked and require no runtime dependencies.
**macOS:**
```bash
brew install ffmpeg pkg-config
CGO_ENABLED=1 go build -o dotmatrix ./cmd/dotmatrix
```
**Ubuntu/Debian:**
```bash
# FFmpeg 8.x may need to be built from source if not available in repos
sudo apt-get install libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev libavdevice-dev
CGO_ENABLED=1 go build -o dotmatrix ./cmd/dotmatrix
```
## Usage
### Command Line
```bash
# From file
dotmatrix image.png
# From URL
dotmatrix https://example.com/image.jpg
# From stdin
curl -s https://example.com/image.jpg | dotmatrix
# With options
dotmatrix --invert --sharpen 50 image.png
# Play MP4 video (subtitles are displayed if embedded)
dotmatrix video.mp4
# Play MP4 at specific framerate
dotmatrix --fps 15 video.mp4
# Capture from webcam (macOS only)
dotmatrix --webcam
# Webcam with options
dotmatrix --webcam --invert --fps 15
```
### As a Library
```go
package main
import (
"image"
"os"
"github.com/kevin-cantwell/dotmatrix"
)
func main() {
img, _, _ := image.Decode(os.Stdin)
dotmatrix.Print(os.Stdout, img)
}
```
## Options
| Flag | Description |
|------|-------------|
| `--invert`, `-i` | Invert colors (for dark backgrounds) |
| `--gamma`, `-g` | Adjust gamma: negative darkens, positive lightens |
| `--brightness`, `-b` | Adjust brightness (-100 to 100) |
| `--contrast`, `-c` | Adjust contrast (-100 to 100) |
| `--sharpen`, `-s` | Sharpen image |
| `--mirror`, `-m` | Flip image horizontally |
| `--mono` | Disable Floyd-Steinberg dithering |
| `--webcam`, `-w` | Capture from webcam (macOS only) |
| `--framerate`, `--fps` | Set playback framerate |
| `--mimeType`, `--mime` | Override auto-detected MIME type |
## Examples
### Sharpened Image
```bash
dotmatrix --sharpen 100 face.jpg
```
```
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣜⢽⡺⣿⣿⣺⣿⣏⣿⣿⣿⣿⢿⣟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣜⡞⣧⢅⢳⡙⣼⣻⡢⡺⡼⣻⢞⡯⣟⣽⢻⡿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣻⣯⣿⣿⣿⡿⡿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡜⣖⢔⠕⢸⡳⡢⡱⠅⡞⣽⠱⢳⢫⡟⡞⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣱⣿⣿⣵⡿⣿⣟⡿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣽⣷⢿⡽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⡿⡽⣿⢿⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⢣⢫⠀⠣⠂⢘⠐⢁⢊⠠⠉⢎⢎⢣⢿⡻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡏⡼⡜⡾⡯⡫⢽⢽⣗⢟⣿⣿⣿⣿⣿⣿⣿⣟⡮⣟⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⡿⡭⡺⣫⣟⡷⣟⢾⢽⢿⣟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣎⠄⢊⢂⠠⠈⠄⠂⠀⢁⢑⠁⢜⡧⡗⢡⢮⠿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣜⢜⢌⢊⠈⣄⢜⢿⡫⣿⣿⣿⣿⣿⣿⣿⡿⡱⣻⢟⣿⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
```
### Animated GIF
Animated GIFs play directly in the terminal with proper timing:

> **Note:** Terminal refresh rates vary. The default macOS Terminal.app works well; iTerm2 may have slower refresh rates.
### MP4 Subtitles
MP4 files with embedded subtitles (mov_text/tx3g format) will display subtitles overlaid at the bottom of the video. Subtitles are:
- Automatically extracted and timed to video playback
- Centered and wrapped to fit the terminal width
- Rendered over the braille output while preserving surrounding pixels
## How It Works
Dotmatrix uses [Unicode Braille Patterns](https://en.wikipedia.org/wiki/Braille_Patterns) (U+2800 to U+28FF) to represent images. Each braille character encodes a 2x4 pixel grid, allowing 256 possible patterns per character.
**Processing pipeline:**
1. **Decode** - Parse input (JPEG, PNG, GIF, BMP, or MP4 video)
2. **Filter** - Apply brightness, contrast, gamma, sharpening adjustments
3. **Scale** - Resize to fit terminal dimensions (2 pixels per column, 4 pixels per row)
4. **Dither** - Convert to monochrome using Floyd-Steinberg diffusion
5. **Encode** - Map each 2x4 pixel block to a braille character
6. **Render** - Output braille characters with newlines (for video, frames are rendered in sequence)
The Floyd-Steinberg dithering algorithm distributes quantization errors to neighboring pixels, preserving the appearance of grayscale gradients in the monochrome output.
## License
MIT
================================================
FILE: braille.go
================================================
package dotmatrix
import (
"image"
"image/color"
"io"
)
// Braille epresents an 8 dot braille pattern in x,y coordinates space. Eg:
// +----------+
// |(0,0)(1,0)|
// |(0,1)(1,1)|
// |(0,2)(1,2)|
// |(0,3)(1,3)|
// +----------+
type Braille [2][4]int
// Rune maps each point in braille to a dot identifier and
// calculates the corresponding unicode symbol.
// +------+
// |(1)(4)|
// |(2)(5)|
// |(3)(6)|
// |(7)(8)|
// +------+
// See https://en.wikipedia.org/wiki/Braille_Patterns#Identifying.2C_naming_and_ordering)
func (b Braille) Rune() rune {
lowEndian := [8]int{b[0][0], b[0][1], b[0][2], b[1][0], b[1][1], b[1][2], b[0][3], b[1][3]}
var v int
for i, x := range lowEndian {
v += int(x) << uint(i)
}
return rune(v) + '\u2800'
}
// String returns a unicode braille character. One of:
// ⣿ ⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾
func (b Braille) String() string {
return string(b.Rune())
}
type BrailleFlusher struct{}
func (BrailleFlusher) Flush(w io.Writer, img image.Image) error {
// An image's bounds do not necessarily start at (0, 0), so the two loops start
// at bounds.Min.Y and bounds.Min.X.
// Looping over Y first and X second is more likely to result in better memory
// access patterns than X first and Y second.
bounds := img.Bounds()
for py := bounds.Min.Y; py < bounds.Max.Y; py += 4 {
for px := bounds.Min.X; px < bounds.Max.X; px += 2 {
var b Braille
// Draw left-right, top-bottom.
for y := 0; y < 4; y++ {
for x := 0; x < 2; x++ {
if px+x >= bounds.Max.X || py+y >= bounds.Max.Y {
continue
}
// Always bet on black!
if img.At(px+x, py+y) == color.Black {
b[x][y] = 1
}
}
}
if _, err := w.Write([]byte(b.String())); err != nil {
return err
}
}
if _, err := w.Write([]byte{'\n'}); err != nil {
return err
}
}
return nil
}
================================================
FILE: doc.go
================================================
/*
Package dotmatrix encodes images in a "dot matrix" pattern using braille unicode characters.
Images are first converted to monochrome, then each 2x4 pixel block is coded to an
8-dot braille character. In this fashion, an image's entire pixel set can be mapped, one-by-one,
to either a "filled" or "unfilled" braille dot. The resulting braille symbols are arranged as
lines of text to form a representation of the original image. The encoded image is
lossy in the sense that color information is reduced to monochrome with diffusion, but otherwise
the resulting text-based image is pixel perfect. In other words, if a pure monochrome image
is used as input, there will be no loss of information.
https://en.wikipedia.org/wiki/Braille_Patterns⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡳⡧⡓⡝⣟⢿⢼⣿⠳⣿⣿⣿⢿⣟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣼⣷⢿⣿⣿⢿⣻⣿⣺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣫⣿⣿⡿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣎⠷⡄⠣⠣⣺⡻⠨⡧⢛⣯⢟⢾⡵⣻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣻⠽⣫⣾⢿⢾⣟⣺⣻⣿⣿⣿⣿⣿⣿⣿⣾⣽⡝⣽⢻⡻⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿
⣿⣿⣿⣿⢟⣿⢿⣿⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡱⡱⡁⠈⡯⠘⢜⠘⢸⠈⡣⣳⢫⠹⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣕⡝⡻⣺⡾⢿⢮⡗⣟⣿⣿⣿⣿⣿⣿⣟⣗⢻⢝⣳⣏⣾⣿⣿⣿⣿⣽⣽⣟⣿⣿
⣿⣿⣿⢑⣝⢯⡯⣾⢻⣻⣽⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣅⠕⠀⠡⠀⠁⠌⠐⠈⠄⢣⠊⣾⡏⡟⢻⣿⣿⣿⣿⣿⣿⣿⣿⣯⡆⢍⠏⢓⠁⠃⣻⣵⢻⣿⣿⣿⣿⣿⣿⢞⢪⢾⣿⢫⣾⣿⣿⣿⣿⣷⣿⣿⣿⣿
⣿⣿⠣⢁⠪⡳⡫⣮⣱⢱⠹⣳⢻⡻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣨⠀⠀⠀⠁⠀⠀⠅⠅⢏⡻⡨⠪⢨⠻⣿⣿⣿⣿⣿⣿⣿⣿⣔⠈⢀⠔⠘⠈⡃⣙⣿⣿⣿⣿⣿⣿⣕⡘⢭⢎⡷⣟⣾⣾⣿⣿⡻⣵⢿⣾⣿
⣿⢇⡊⡄⡏⢉⡫⣫⢝⢮⠺⣔⠭⡚⢐⠜⢻⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⡀⠀⠀⠀⠀⠡⠃⠐⠈⠀⠁⠄⠹⢿⣿⣿⣿⣿⣿⣿⣷⡁⢂⠁⠀⢀⠐⠅⠚⣿⣿⣿⣿⣿⣿⠂⠢⡳⣻⣺⣿⢿⣿⣿⣟⣳⢻⣵⣿
⣿⡇⣫⠂⡀⠃⠀⠅⡝⣒⠳⠌⠕⠁⠂⠈⠀⠈⠈⠙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢘⣿⣿⣿⣿⣿⣿⣿⣮⠀⠀⠀⠀⠀⠀⢍⠹⣿⣿⣿⣿⣿⢅⢪⡽⡽⣹⣜⣽⣿⣿⡯⢺⢫⣻⣿
⣿⣾⣵⣶⣷⣬⣅⠅⠌⠘⡐⣈⠁⡀⠄⠀⠀⠀⠀⠀⠀⠀⠙⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⣿⣿⣿⡆⠀⠀⠀⠀⠀⠥⢐⣽⣿⣿⣿⣿⡣⠈⡩⣓⣵⡻⢽⣿⣿⢗⣕⣗⢷⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣭⣥⡉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡄⠀⠀⠀⠀⠀⠀⠀⠈⢿⣿⣿⣿⣿⣿⣷⡆⠀⠀⠀⠀⠀⠈⢽⣿⣿⣿⢿⡕⠈⠴⠵⢵⢶⣿⣿⣿⡗⣪⣼⢞⣷
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡄⠀⠀⠀⠀⠀⠀⠈⢹⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⠀⢼⣿⣿⣿⣷⠕⠈⠠⢦⢵⣻⣿⣿⣿⣞⣝⢿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣶⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⣧⠀⠀⠀⠀⠀⠨⣿⣿⣿⣿⠡⠀⡈⠀⠩⣿⣿⣿⣟⢇⠃⣗⣿⣟
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠻⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⠀⠀⠀⠀⠈⢻⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠈⣿⣿⣿⣿⠀⠀⠀⠀⠑⣿⣿⣿⣿⡥⠠⣽⣿⣿
⣿⣿⢯⢿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣄⡀⠀⠀⠀⠀⠀⠀⠉⠻⣿⣿⣿⣿⣿⣿⣿⣦⠀⠀⠀⠀⠀⠈⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⣿⣿⣿⣷⠀⠀⠀⠀⢱⣿⣿⣿⣟⠎⣪⣿⣿⣪
⣿⣿⡯⡻⡙⡋⠉⠪⠁⠀⠀⠀⠀⠀⠀⠉⠉⠛⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⠀⠀⠀⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣷⡄⠀⠀⠀⠀⠘⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⣿⣿⣿⡿⠀⠀⠀⠀⢿⣿⣿⣿⡫⢢⣿⣿⣯⢷
⣿⡿⣯⡪⡂⡄⢄⢁⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠛⠿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣄⠀⠀⠀⠀⠀⠈⠻⣿⣿⣿⣿⣿⣷⠀⠀⠀⠀⠀⢹⣿⣿⣿⣿⠀⠀⠀⠀⠀⣿⣿⣿⡇⠀⠀⠀⣐⣾⣿⡿⡧⢑⢶⣿⡟⣳⣿
⠀⠈⢳⣳⣽⣿⣾⣶⣿⣯⡶⢴⣴⣤⣤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠛⠿⣿⣿⣿⣿⣿⣿⣷⣤⡀⠀⠀⠀⠀⠉⢻⣿⣿⣿⣿⣧⠀⠀⠀⠀⠸⣿⣿⣿⣿⠀⠀⠀⠀⢀⣿⣿⣿⠇⠀⠀⠀⣽⣿⡿⡏⡐⢸⣿⣟⢳⣷⣿
⣤⣶⣽⣟⣿⣿⣿⣿⣿⢕⣽⢽⣺⢿⣿⣿⣿⣶⣦⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠙⠛⢿⣿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠀⠻⣿⣿⣿⣿⣧⠀⠀⠀⠀⣿⣿⣿⣿⠀⠀⠀⠀⢰⣿⣿⣿⠁⠀⠀⢠⣿⣿⠽⠨⣮⣾⣿⣣⣿⣾⠟
⣷⣿⣿⣿⣿⣿⣿⣟⢎⠧⡫⣻⢟⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⡀⠀⠀⠀⠀⠀⠈⠛⢿⣿⣿⣿⣿⣦⡀⠀⠀⠀⠘⢿⣿⣿⣿⡄⠀⠀⠀⢹⣿⣿⣿⠀⠀⠀⠀⣼⣿⣿⡏⠀⠀⠀⣼⣿⠇⠀⣼⣿⡿⣪⣿⣿⠋⢀
⣿⣿⣿⣿⣿⣿⣿⠁⠁⠀⠀⠀⠀⠉⠉⠉⠛⠛⠻⢿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣄⡀⠀⠀⠀⠀⠙⠻⣿⣿⣿⣿⣦⡀⠀⠀⠈⢻⣿⣿⣷⡀⠀⠀⠈⣿⣿⣯⠀⠀⠀⢀⣿⣿⡿⠁⠀⠀⢰⣿⡟⠀⢰⣿⡟⢝⢸⡟⠁⣠⣾
⣿⣿⠏⠑⢽⡻⢑⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠻⢿⣿⣿⣿⣿⣿⣷⣦⣤⡀⠀⠀⠀⠈⠙⣿⣿⣿⣷⣄⠀⠀⠀⠻⣿⣿⣧⠀⠀⠀⢿⣿⣷⠀⠀⠀⢸⣿⣿⠃⠀⠀⢀⣿⣿⠁⠀⣾⠉⢓⠠⡿⠅⣴⣿⣿
⣿⣯⠀⢈⠕⡌⠆⠀⣠⣠⣤⣤⣤⣤⣄⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠛⠻⢿⣿⣿⣿⣿⣷⣤⡀⠀⠀⠀⠙⢿⣿⣿⣦⡀⠀⠀⠹⣿⣿⠀⠀⠀⢹⣿⣿⠀⠀⠀⢸⣿⡿⠀⠀⠀⣼⣿⠃⠀⣸⠃⠀⠪⣾⡫⣲⣿⣿⣿
⣿⠁⠀⠀⢕⢨⣺⣽⢝⡮⡷⡵⣿⣿⣿⣿⣿⣿⣶⣦⣤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠛⠿⣿⣿⣿⣶⣤⠀⠀⠈⠙⣿⣿⣷⣄⠀⠀⠻⣿⣇⠀⠀⢸⣿⣿⠀⠀⠀⣾⣿⠃⠀⠀⣼⣿⠃⠀⣼⠏⠀⢠⣿⣾⣿⣿⣿⣿⣿
⡿⠀⠀⢈⣦⣿⣿⡯⣽⣺⢝⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣦⣤⣀⡀⠀⠀⠀⠀⠀⠀⠉⠛⠻⢿⣿⣦⣀⠀⠈⠛⣿⣿⣦⡀⠀⠙⣿⡆⠀⠘⣿⣿⠀⠀⢐⣿⡟⠀⢀⣔⠟⠃⢀⡼⠋⠀⢀⣾⣿⣿⢿⣿⣿⠟⠁
⣿⠠⢲⣻⣽⣿⣿⡝⠚⠊⠓⠛⠛⠛⠛⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⢿⢿⣿⣿⣿⣶⣶⣤⣤⣀⠀⠀⠀⠀⠉⠛⠻⢷⣤⡀⠈⠙⢿⣷⣦⡀⠘⢿⡄⠀⣹⣿⣀⣀⣿⡋⢀⣠⠾⠋⣀⡴⠋⠁⠀⢠⣾⣿⠋⠃⠻⠛⠁⠀⠀
⣿⣴⣾⣿⣿⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠐⢐⣶⣶⣾⣾⢾⣶⣶⣶⣶⣶⣦⣭⣍⠛⢛⠛⠿⠿⣿⣷⣶⣦⣄⣀⠀⠀⠉⠛⠶⣦⢤⣽⣿⣷⣶⣾⣿⣿⣿⣿⣿⡿⢿⣿⣿⣿⡒⠚⠉⠁⢀⣤⣾⣿⣿⠓⠀⢰⠆⠀⠀⠀⠀
⣿⣿⣿⣿⣿⢟⠅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⣛⣿⠛⠃⠈⠻⣛⠛⠛⣻⠋⠛⣿⣿⣶⣬⡠⠀⠀⠀⠀⠉⠉⠙⠛⠛⠒⠲⠒⢒⣫⣵⢿⣿⠋⠙⠿⣋⣋⣻⠛⠁⠁⠛⣻⣯⣶⣶⣶⣾⣿⣿⠿⠛⠁⠀⢀⡺⡧⠀⣠⣴⣾
⣿⣿⣏⠁⠁⡃⠆⠀⠀⠀⢀⣠⣠⣤⣤⣶⣶⣿⣿⣿⣦⣄⣀⠈⠉⠉⠀⢀⣀⢻⣾⣿⣿⣿⣷⣦⣤⣤⣤⣤⣤⣤⣤⣤⣤⠖⠋⠉⣠⣟⠻⣱⣤⣤⣤⣅⣀⣤⣤⣶⣿⠿⠟⠛⠋⠉⠉⠉⠀⠀⠀⠀⢠⣞⣮⣿⣿⣿⣿⣿
⣿⣿⣿⣦⡀⠀⠁⢠⢔⢾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⠋⠈⠀⠀⠈⠙⠙⠋⠙⠉⠉⠉⠉⠉⠁⣤⣶⠟⠋⣡⠔⣩⢺⢿⢻⠯⡙⠓⠶⣤⣤⣤⣤⣤⣤⣶⣶⣶⣶⣶⣶⣾⣿⢵⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣶⣷⣮⢇⢯⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠿⠟⠛⠛⠉⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣴⣾⠛⠁⡴⠊⣡⠖⡅⣨⠏⡜⡄⠹⣶⣤⡀⠉⠉⠉⠋⠛⠛⠛⠛⠛⠛⠛⠋⠂⢸⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡧⣫⢻⣿⣿⣿⠿⠛⠛⠉⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⣠⣤⣤⣴⣶⣶⣶⣾⣿⡿⠛⠁⣠⡞⢀⡴⠁⡌⢠⡏⡎⢇⢻⡄⠉⠻⣿⣿⣶⣦⣤⣤⣤⣀⣀⡀⡀⠀⠀⠀⣾⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⡪⠟⠉⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣶⣿⣿⣿⣿⣿⣿⠿⠛⠛⠉⠁⠀⢠⣴⠛⢡⣾⠃⣼⠁⣼⡇⢧⠸⣤⠹⣦⡀⠀⠉⠛⠻⢿⣿⣿⣿⣿⣿⣿⣿⡪⣪⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⠅⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣤⣴⣾⣿⣿⣿⣿⠿⠛⠉⠉⠁⠀⠀⠀⠀⠀⣠⣶⡿⠃⣠⣿⠃⢠⡏⠀⣿⡇⢹⡇⠹⣧⠙⢿⣦⣄⠀⠀⠀⠈⠙⠛⠻⢿⣿⡿⡱⣽⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡀⠀⠀⠀⠀⠀⠀⣀⣠⣴⣶⣿⣿⣿⣿⣿⣿⡿⠛⠉⠁⠀⠀⠀⠀⠀⠀⢀⣠⣶⣿⣿⠏⠀⢰⣿⡏⠀⣼⠁⠀⣿⡇⠈⣿⠀⠹⣦⠈⠛⣿⣷⣦⣄⠀⠀⠀⠀⠀⠀⠉⠀⣸⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⠀⠀⢀⣠⣶⣾⣿⣿⣿⣿⣿⣿⣿⠿⠛⠉⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣿⣿⣿⠟⠀⠀⢠⣾⡟⠀⢰⡟⠀⠀⣿⡇⠀⢻⣇⠀⠹⣷⡀⠈⠛⢿⣿⣿⣦⣄⠀⠀⠀⠀⢀⣾⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣶⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⠁⠀⠀⠀⠀⠀⠀⠀⢀⣤⣶⣿⣿⣿⡿⠛⠁⠀⠀⢠⣿⣿⠇⠀⢸⡇⠀⠐⣿⣷⠀⢸⣿⡆⠀⢹⣿⣄⠀⠈⠙⢿⣿⣿⣿⣶⣶⡴⣺⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⣠⣶⣿⣿⣿⣽⠿⠋⠀⠀⠀⢀⣴⣿⣿⡿⠀⠀⣸⡇⠀⢸⣿⣿⠀⠀⣿⣷⠀⠀⢿⣿⣦⠀⠀⠀⠙⠻⣿⣿⣿⠕⣹⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠛⠉⠀⠀⠀⠀⠀⠀⠀⣠⣴⣾⣿⣿⣿⡿⣫⣑⣦⣄⠀⠀⣠⣼⣿⣿⣟⣥⣴⣴⠟⠁⠀⢸⣿⣿⠀⠀⢹⣿⣦⠀⠈⢻⣿⣷⣄⠀⠀⠀⠈⠉⠁⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⡟⠋⠀⠀⠑⠁⢙⣶⣾⣿⣿⠟⠉⠉⢡⣿⣿⠀⠀⠀⢸⣿⣿⡄⠀⠀⣿⣿⣇⠀⠈⠻⣿⣿⣷⣄⣀⢀⠀⡀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠀⠀⣰⣾⣿⣿⣿⣿⣿⠋⠀⠀⠀⠀⠀⢀⣼⣿⣿⣿⠃⠀⠀⠀⢸⣿⣿⠀⠀⠀⠘⣿⣿⣷⡀⠀⢸⣿⣿⡄⠀⠀⠙⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠀⠀⢠⣾⣿⣿⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀⢀⣾⣿⣿⣿⠋⠀⠀⠀⠀⣾⣿⣿⠀⠀⠀⠀⣿⣿⣿⣧⠀⠀⢻⣿⣿⡄⠀⠀⠈⠛⠿⢿⠿⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣆⠀⠀⣶⣿⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⠀⢠⣾⣿⣿⣿⡟⠀⠀⠀⠀⢠⣿⣿⡿⠀⠀⠀⠀⣿⣿⣿⣿⡄⠀⠀⢻⣿⣷⡄⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣾⣿⣿⣿⣿⣿⡿⠉⠀⠀⢀⡀⡀⣀⣠⣿⣿⣿⣿⣿⠁⠀⠀⠀⠀⣾⣿⣿⡯⠀⠀⠀⠀⣿⣿⣿⣿⣧⠀⠀⠈⣿⣿⣿⣦⡀⠀⢀⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣿⣿⣿⣿⡟⠁⠀⠀⠀⠉⠙⠛⣻⣿⣿⣿⣿⣿⠛⠓⠒⠒⠒⢳⣿⣿⣿⡟⠒⠒⠓⠛⣿⣿⣿⣿⣿⡄⠀⠀⠘⣿⣿⣿⣿⠗⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡗⠈⢻⣿⠏⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⠏⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⠀⠀⠀⠀⢸⣿⣿⣿⣿⣷⠀⠀⠀⠘⣿⡿⠉⠀⢽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣽⣿⡣⠀⠈⡋⠀⠀⠀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⠃⠀⠀⠀⠀⠀⢀⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣧⠀⠀⠀⣸⡅⠀⠀⠀⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣹⣾⣿⠂⠀⢐⠨⠢⡀⠀⠀⠀⠀⣴⣿⣿⣿⣿⣿⡿⠁⠣⠲⠶⡲⢖⣾⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⢸⣿⣿⣿⣿⣿⣇⣠⣼⣿⣷⡀⠀⠀⠈⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢏⢏⡾⣟⣿⠠⠀⠄⢂⠑⠔⡄⠀⢠⣾⣿⣿⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⠀⠀⠀⠀⠀⠀⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡦⠀⠀⠈⠻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡫⠣⡱⡵⣻⣿⣯⢃⠀⢐⠠⠈⡊⢼⣶⣾⣿⣿⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣆⠀⠀⠀⠀⠀⠈⢻⡿⠛⣿⣿⣿⣿⣿⣿⣿⣿⡥⡀⠀⠀⠑⠹⢿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⡿⡟⠝⡑⢌⢪⢪⢞⣯⡷⡯⡂⠄⠀⡂⠐⠨⡸⣿⣿⣿⣿⣿⣿⡟⠁⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⡄⠀⠀⠀⠀⢀⠌⠀⠀⠘⢹⣿⣿⣿⣿⣿⣿⣿⣧⡂⠀⠀⠀⠁⠩⠻⣻⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⢟⠫⡊⠌⢌⢐⢅⢎⢮⣻⣺⢽⢝⠔⢀⠡⠀⠌⡐⢼⣿⣿⣿⣿⣿⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣷⠀⠀⠠⢐⠁⠌⠠⠀⠀⠀⠙⢻⣿⣿⣿⣿⣿⣿⣾⡢⡀⠠⠐⢀⠡⠈⠝⡻⣿⣿⣿
⣿⡿⡟⡝⢌⠢⡑⠌⢌⠢⡂⡇⣗⢽⣺⡽⣽⢱⢁⠂⠄⡁⠂⠠⢱⣿⣿⣿⣿⣿⣿⣷⣤⡀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿⣿⣿⣿⣿⣿⣶⡌⢈⠠⠈⠀⠂⠀⠀⠀⠀⠀⠛⢿⣿⣿⣿⣿⣿⣷⡪⡢⢈⠄⠂⢅⠑⢌⠢⠫⢿
⡫⡪⠪⡨⠢⡑⠌⢜⢐⢑⢌⢮⡺⣝⣗⣟⢮⢣⠂⠌⡠⠀⠡⠈⠠⣻⣿⣿⣿⣿⣿⣿⣿⣿⢑⢲⡰⡤⡤⣄⢼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣗⡀⠀⢀⠈⡀⠂⠀⠈⢀⠀⠀⠈⠻⢿⣿⣿⣿⣿⣯⣞⢔⠌⢌⢐⢈⠢⢑⠍⡪
⢸⠨⡊⡢⡑⢌⢊⠢⡑⡅⢇⠧⡯⣳⢯⣞⡝⡆⢅⠅⡂⠌⡐⢈⠀⢝⣿⣿⣿⣿⣿⣿⣿⣿⢜⠔⢕⢝⢜⢜⢜⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⡐⠀⠄⢀⠈⠄⡁⠄⠠⠀⠀⠈⡈⢏⢿⣿⣿⣿⣯⣷⢍⢆⠢⡂⠌⡂⡑⢌
⡘⢌⠢⡑⢌⢢⠡⡃⢕⢸⢨⡫⣞⡽⣳⢵⢝⢜⢐⢐⢀⢂⠐⠄⢂⢐⢽⣿⣿⣿⣿⣿⣿⣿⡇⡕⢕⢌⢂⠕⡨⢚⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡡⠐⢀⠀⢂⠀⡂⠐⡀⠁⠄⢐⢀⠣⡫⣻⣿⣿⣿⣟⣎⡇⡆⢅⠢⡈⡢
⢸⠨⡊⡌⢆⢕⢑⠜⡌⡎⢮⢮⡳⣝⣗⣝⢮⢊⡂⡂⡢⠐⡈⢐⢀⠂⢽⣿⣿⣿⣿⣿⣿⣿⣯⡪⡪⡢⡑⢌⠔⡈⡺⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡅⡂⠠⠀⠂⠀⠂⠄⠅⠨⠀⠄⢂⠪⡘⡎⡯⡿⣿⣷⣻⣜⢢⠡⢂⠢
⠢⡃⡪⡨⡒⡬⡢⡣⡣⡹⣪⡳⡽⣕⢗⡎⡎⡖⡨⢂⠢⡁⡂⡂⡐⠨⡘⣿⣿⣿⣿⣿⣿⣿⣿⢮⢢⢑⠌⣂⠪⢐⠸⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡔⡅⠌⠠⠈⡐⠀⠌⠠⠁⡂⢐⠨⠐⡡⢱⢹⢽⣿⢿⣺⣜⢌⠢⡨
⢕⢕⢜⡔⣕⢕⢕⢕⢕⢝⣜⢮⢳⢕⢗⡕⡇⡕⢌⠢⡑⡐⡐⠠⠐⠡⠨⣻⣿⣿⣿⣿⣿⣿⣿⣿⡰⢐⠌⡂⠅⠅⡘⠸⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣮⡐⠄⠁⠄⠂⡈⠀⠅⡐⡐⠠⡁⡂⢅⢣⡫⣾⢿⣿⢾⡕⡕⡔
⡷⡯⣷⢽⡮⡷⣝⢮⢮⡣⣗⢵⢹⢪⡣⡣⡣⡊⡢⡑⢔⢐⠌⠄⠡⠁⠅⢯⣿⣿⣿⣿⣿⣿⣿⣷⡇⡂⠐⠀⡁⠅⢀⠁⠕⡻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣗⣕⠅⢌⠐⡀⠅⢂⠢⠨⡢⢢⢱⢱⢱⡹⡮⡿⣽⣟⣮⢷⢵
⢝⢝⢪⢫⢪⢫⢪⢫⢺⢺⢸⢪⢳⡱⡕⡕⡕⡌⡆⣊⠢⡂⡊⠨⢀⠁⡈⢎⣿⣿⣿⣿⣿⣿⣿⣿⣗⢄⠡⠀⠠⠈⢄⠐⡀⠌⢘⢝⢿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣾⡽⣦⡣⡐⡅⡢⡑⡕⡜⡎⡗⡝⡕⡝⡎⡏⢞⢜⢎⢏⢏
⡑⢅⠣⡊⡢⢑⠅⠕⢅⠣⡃⢇⢇⢇⢏⢪⠪⡊⡎⢔⢑⠔⢌⠌⠠⠐⡀⠕⣝⣿⣿⣿⣿⣿⣿⣿⣗⠅⠀⠀⠀⠌⠠⠂⠄⠠⠀⠄⠩⡫⡺⡯⣻⣝⢯⣟⢿⢽⡺⡯⣗⢯⢷⡱⡜⢌⠪⡘⡌⡪⠪⡘⢌⠢⡊⣊⢢⢑⢅⢒
*/
package dotmatrix
================================================
FILE: dotmatrix_suite_test.go
================================================
package dotmatrix_test
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestDotmatrix(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Dotmatrix Suite")
}
================================================
FILE: ffmpeg_check.go
================================================
package dotmatrix
/*
#cgo pkg-config: libavcodec libavformat libavutil
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
// Require FFmpeg 8.x (libavcodec >= 61, libavformat >= 61, libavutil >= 59)
// These version numbers correspond to FFmpeg n8.0
#if LIBAVCODEC_VERSION_MAJOR < 61
#error "FFmpeg 8.0 or later is required. Found libavcodec version too old. Please install FFmpeg 8.x: https://ffmpeg.org/download.html"
#endif
#if LIBAVFORMAT_VERSION_MAJOR < 61
#error "FFmpeg 8.0 or later is required. Found libavformat version too old. Please install FFmpeg 8.x: https://ffmpeg.org/download.html"
#endif
#if LIBAVUTIL_VERSION_MAJOR < 59
#error "FFmpeg 8.0 or later is required. Found libavutil version too old. Please install FFmpeg 8.x: https://ffmpeg.org/download.html"
#endif
*/
import "C"
================================================
FILE: gif.go
================================================
package dotmatrix
import (
"context"
"image"
"image/color"
"image/gif"
"io"
"time"
)
type GIFPrinter struct {
w io.Writer
c Config
}
func NewGIFPrinter(w io.Writer, c *Config) *GIFPrinter {
return &GIFPrinter{
w: w,
c: mergeConfig(c),
}
}
/*
Print animates a gif
*/
func (p *GIFPrinter) Print(ctx context.Context, giff *gif.GIF) error {
if len(giff.Image) < 1 {
return nil
}
// Only used if we see background disposal methods
bgPallette := []color.Color{color.Transparent}
if giff.Config.ColorModel != nil {
bgPallette = giff.Config.ColorModel.(color.Palette)
}
// The screen is what we flush to the writer on each iteration
screen := redraw(image.NewPaletted(giff.Image[0].Bounds(), bgPallette), p.c.Filter, p.c.Drawer)
rows := screen.Bounds().Dy() / 4
if screen.Bounds().Dy()%4 != 0 {
rows++
}
for c := 0; giff.LoopCount == 0 || c < giff.LoopCount; c++ {
for i := 0; i < len(giff.Image); i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
delay := time.After(time.Duration(giff.Delay[i]) * time.Second / 100)
frame := redraw(giff.Image[i], p.c.Filter, p.c.Drawer)
switch giff.Disposal[i] {
case gif.DisposalPrevious: // Dispose previous essentially means draw then undo
temp := image.NewPaletted(screen.Bounds(), screen.Palette)
copy(temp.Pix, screen.Pix)
p.drawOver(screen, frame)
if err := flush(p.w, screen, p.c.Flusher); err != nil {
return err
}
<-delay
screen = temp
case gif.DisposalBackground: // Dispose background replaces everything just drawn with the background canvas
background := redraw(image.NewPaletted(frame.Bounds(), bgPallette), p.c.Filter, p.c.Drawer)
p.drawExact(screen, background)
temp := image.NewPaletted(screen.Bounds(), screen.Palette)
copy(temp.Pix, screen.Pix)
p.drawOver(screen, frame)
if err := flush(p.w, screen, p.c.Flusher); err != nil {
return err
}
<-delay
screen = temp
default: // Dispose none or undefined means we just draw what we got over top
p.drawOver(screen, frame)
if err := flush(p.w, screen, p.c.Flusher); err != nil {
return err
}
<-delay
}
p.c.Reset(p.w, rows)
}
}
return nil
}
// Draws any non-transparent pixels into target
func (p *GIFPrinter) drawOver(target *image.Paletted, source image.Image) {
bounds := source.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
c := source.At(x, y)
if c == color.Transparent {
continue
}
target.Set(x, y, c)
}
}
}
// Draws pixels into target, including transparent ones.
func (p *GIFPrinter) drawExact(target *image.Paletted, source image.Image) {
bounds := source.Bounds()
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
for x := bounds.Min.X; x < bounds.Max.X; x++ {
target.Set(x, y, source.At(x, y))
}
}
}
================================================
FILE: go.mod
================================================
module github.com/kevin-cantwell/dotmatrix
go 1.22.0
toolchain go1.22.2
require (
github.com/asticode/go-astiav v0.40.0
github.com/disintegration/imaging v1.6.2
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.36.2
github.com/urfave/cli/v2 v2.27.5
golang.org/x/image v0.23.0
golang.org/x/term v0.28.0
)
require (
github.com/asticode/go-astikit v0.42.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
github.com/fsnotify/fsnotify v1.4.9 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/net v0.33.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
================================================
FILE: go.sum
================================================
github.com/asticode/go-astiav v0.40.0 h1:7El8FyONtjPOmcmqR+zjjihG7ysum5RtTVIhIMrLFQM=
github.com/asticode/go-astiav v0.40.0/go.mod h1:GI0pHw6K2/pl/o8upCtT49P/q4KCwhv/8nGLlCsZLdA=
github.com/asticode/go-astikit v0.42.0 h1:pnir/2KLUSr0527Tv908iAH6EGYYrYta132vvjXsH5w=
github.com/asticode/go-astikit v0.42.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=
github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc=
github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM=
github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w=
github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
================================================
FILE: image.go
================================================
package dotmatrix
import (
"fmt"
"image"
"image/color"
"image/draw"
"io"
)
// Flushes an image to the io.Writer. E.g. by using braille characters.
type Flusher interface {
Flush(w io.Writer, img image.Image) error
}
// Filter may alter an image in any way, including resizing it.
// It is applied prior to drawing the image in the dotmatrix palette.
type Filter interface {
Filter(image.Image) image.Image
}
type noop struct{}
func (noop) Filter(img image.Image) image.Image {
return img
}
type Config struct {
Filter Filter
Flusher Flusher
Drawer draw.Drawer
// Reset is invoked between animated frames of an image. It can be used to
// apply custom cursor positioning.
Reset func(w io.Writer, rows int)
}
var defaultConfig = Config{
Filter: noop{},
Flusher: BrailleFlusher{},
Drawer: draw.FloydSteinberg,
}
func mergeConfig(c *Config) Config {
if c == nil {
return defaultConfig
}
if c.Filter == nil {
c.Filter = defaultConfig.Filter
}
if c.Drawer == nil {
c.Drawer = defaultConfig.Drawer
}
if c.Flusher == nil {
c.Flusher = defaultConfig.Flusher
}
if c.Reset == nil {
c.Reset = func(w io.Writer, rows int) {
fmt.Fprintf(w, "\033[999D\033[%dA", rows)
}
}
return *c
}
var defaultPalette = []color.Color{color.Black, color.White, color.Transparent}
type Printer struct {
w io.Writer
c Config
}
func Print(w io.Writer, img image.Image) error {
return NewPrinter(w, &defaultConfig).Print(img)
}
// NewPrinter provides an Printer. If drawer is nil, draw.FloydSteinberg is used.
func NewPrinter(w io.Writer, c *Config) *Printer {
return &Printer{
w: w,
c: mergeConfig(c),
}
}
/*
Print prints the image as a series of braille and line feed characters and writes
to w. Braille symbols are useful for representing monochrome images
because any 2x4 pixel area can be represented by one of unicode's
256 braille symbols. See: https://en.wikipedia.org/wiki/Braille_Patterns
Each pixel of the image is converted to either black or white by redrawing the
image using the printer's drawer (Floyd Steinberg diffusion, by default) and a
3-color palette of black, white, and transparent. Finally, each 2x4 pixel block
is printed as a braille symbol.
As an example, this output was printed from a 134px by 108px image of Saturn:
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡿⡻⡫⡫⡣⣣⢣⢇⢧⢫⢻⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡟⡟⣝⣜⠼⠼⢚⢚⢚⠓⠷⣧⣇⠧⡳⡱⣻⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡟⣏⡧⠧⠓⠍⡂⡂⠅⠌⠄⠄⠄⡁⠢⡈⣷⡹⡸⣪⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⠿⢿⢿⢿⢟⢏⡧⠗⡙⡐⡐⣌⢬⣒⣖⣼⣼⣸⢸⢐⢁⠂⡐⢰⡏⣎⢮⣾⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣽⣾⣶⣿⢿⢻⡱⢕⠋⢅⠢⠱⢼⣾⣾⣿⣿⣿⣿⣿⣿⣿⡇⡇⠢⢁⢂⡯⡪⣪⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⠏⢎⠪⠨⡐⠔⠁⠁⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⢱⠡⡁⣢⢏⢮⣾⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⢍⢆⢃⢑⠤⠑⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣿⣿⣿⣿⡿⡱⢑⢐⢼⢱⣵⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⢫⡱⢊⢂⢢⠢⡃⠌⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⣿⢟⢑⢌⢦⢫⣪⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡻⡱⡑⢅⢢⣢⣳⢱⢑⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⡑⡑⡴⡹⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⢝⠜⠨⡐⣴⣵⣿⣗⡧⡣⠢⢈⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣜⢎⣷⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡫⡱⠑⡁⣌⣮⣾⣿⣿⣿⣟⡮⡪⡪⡐⠠⠀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡟⢏⠜⠌⠄⣕⣼⣿⣿⣿⣿⣿⣿⣯⡯⣎⢖⠌⠌⠄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢨⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⢕⠕⢁⠡⣸⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⡽⡮⡪⡪⠨⡂⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⢕⠕⢁⢐⢔⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢽⡱⡱⡑⡠⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢟⢕⠕⢁⢐⢰⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣞⢜⠔⢄⠡⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⡿⡹⡰⠃⢈⠠⣢⣿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡮⣇⢏⢂⠢⠀⠀⠀⠀⠀⠀⠀⣠⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⢫⢒⡜⠐⠀⢢⣱⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣳⢕⢕⠌⠄⡀⠀⠀⢀⣤⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⡿⡑⣅⠗⠀⡀⣥⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠟⢙⠙⠿⣿⣿⣿⣿⣿⣿⣿⣿⣯⢮⡪⣂⣢⣬⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⡟⡜⢌⡞⡀⣡⣾⣿⣿⣿⣿⣿⣿⣿⡿⠛⠉⢀⡠⠔⢜⣱⣴⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⡿⡸⡘⢜⣧⣾⣿⣿⣿⣿⣿⣿⠿⢛⡡⠤⡒⢪⣑⣬⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⡇⡇⡣⣷⣿⣿⣿⣿⣿⠿⡛⡣⡋⣕⣬⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣮⣺⣿⣿⣟⣻⣩⣢⣵⣾⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿⠿
*/
func (p *Printer) Print(img image.Image) error {
img = redraw(img, p.c.Filter, p.c.Drawer)
return flush(p.w, img, p.c.Flusher)
}
func redraw(img image.Image, filter Filter, drawer draw.Drawer) *image.Paletted {
origBounds := img.Bounds()
img = filter.Filter(img)
newBounds := img.Bounds()
scaleX := float64(newBounds.Dx()) / float64(origBounds.Dx())
scaleY := float64(newBounds.Dy()) / float64(origBounds.Dy())
// The offset is important because not all images have bounds starting at (0, 0), and
// the filter may accidentally zero the min bounding point.
offset := image.Pt(int(float64(origBounds.Min.X)*scaleX), int(float64(origBounds.Min.Y)*scaleY))
// Create a new paletted image using a monochrome+transparent color palette.
paletted := image.NewPaletted(img.Bounds(), defaultPalette)
paletted.Rect = paletted.Bounds().Add(offset)
drawer.Draw(paletted, paletted.Bounds(), img, img.Bounds().Min)
return paletted
}
func flush(w io.Writer, img image.Image, flusher Flusher) error {
return flusher.Flush(w, img)
}
================================================
FILE: image_test.go
================================================
package dotmatrix
// import (
// . "github.com/onsi/ginkgo"
// . "github.com/onsi/gomega"
// )
// // ⡪⣛
// //
// var testCanvas = canvas{
// { // mx1
// braille{ // my1
// {white, black, white, black}, // x1,y1-4
// {black, white, black, white}, // x2,y1-4
// },
// braille{ // my1
// {black, black, black, black}, // x1,y1-4
// {transparent, transparent, transparent, transparent}, // x2,y1-4
// },
// },
// { // mx2
// braille{ // my2
// {black, black, white, black}, // x1,y1-4
// {black, black, white, black}, // x2,y1-4
// },
// braille{ // my2
// {transparent, transparent, transparent, transparent}, // x1,y1-4
// {black, black, black, black}, // x2,y1-4
// },
// },
// }
// var _ = Describe("canvas", func() {
// Describe("#At", func() {
// It("Should return the color at the given coordinate.", func() {
// // copy(c, testCanvas)
// Expect("\n" + testCanvas.String()).To(Equal("\n⡪⣛\n"))
// })
// })
// })
================================================
FILE: mise.toml
================================================
[tools]
go = "1.22"
================================================
FILE: mp4.go
================================================
package dotmatrix
import (
"context"
"encoding/binary"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/asticode/go-astiav"
)
// subtitle holds a decoded subtitle with timing information.
type subtitle struct {
startPTS int64
endPTS int64
text string
}
// MP4Printer prints MP4 video frames as braille characters.
type MP4Printer struct {
w io.Writer
c Config
}
// NewMP4Printer creates a new MP4Printer.
func NewMP4Printer(w io.Writer, c *Config) *MP4Printer {
return &MP4Printer{
w: w,
c: mergeConfig(c),
}
}
// Print plays an MP4 video from a file path. If fps is less than zero, it will
// use the video's native framerate. Otherwise, fps dictates how many frames per
// second are printed.
func (p *MP4Printer) Print(ctx context.Context, inputPath string, fps int) error {
// Allocate packet
pkt := astiav.AllocPacket()
if pkt == nil {
return fmt.Errorf("mp4: failed to allocate packet")
}
defer pkt.Free()
// Allocate frame
frame := astiav.AllocFrame()
if frame == nil {
return fmt.Errorf("mp4: failed to allocate frame")
}
defer frame.Free()
// Allocate format context
formatCtx := astiav.AllocFormatContext()
if formatCtx == nil {
return fmt.Errorf("mp4: failed to allocate format context")
}
defer formatCtx.Free()
// Open input
if err := formatCtx.OpenInput(inputPath, nil, nil); err != nil {
return fmt.Errorf("mp4: opening input failed: %w", err)
}
defer formatCtx.CloseInput()
// Find stream info
if err := formatCtx.FindStreamInfo(nil); err != nil {
return fmt.Errorf("mp4: finding stream info failed: %w", err)
}
// Find video and subtitle streams
var videoStream *astiav.Stream
var videoStreamIdx int
var subtitleStream *astiav.Stream
var subtitleStreamIdx int
for _, s := range formatCtx.Streams() {
switch s.CodecParameters().MediaType() {
case astiav.MediaTypeVideo:
if videoStream == nil {
videoStream = s
videoStreamIdx = s.Index()
}
case astiav.MediaTypeSubtitle:
if subtitleStream == nil {
subtitleStream = s
subtitleStreamIdx = s.Index()
}
}
}
if videoStream == nil {
return fmt.Errorf("mp4: no video stream found")
}
// Find decoder
codec := astiav.FindDecoder(videoStream.CodecParameters().CodecID())
if codec == nil {
return fmt.Errorf("mp4: decoder not found for codec %s", videoStream.CodecParameters().CodecID())
}
// Allocate codec context
codecCtx := astiav.AllocCodecContext(codec)
if codecCtx == nil {
return fmt.Errorf("mp4: failed to allocate codec context")
}
defer codecCtx.Free()
// Copy codec parameters
if err := videoStream.CodecParameters().ToCodecContext(codecCtx); err != nil {
return fmt.Errorf("mp4: copying codec parameters failed: %w", err)
}
// Open codec
if err := codecCtx.Open(codec, nil); err != nil {
return fmt.Errorf("mp4: opening codec failed: %w", err)
}
// Set up subtitle storage if subtitle stream exists
var subtitles []subtitle
var subtitleTimeBase astiav.Rational
if subtitleStream != nil {
subtitleTimeBase = subtitleStream.TimeBase()
}
// Create software scale context for converting to RGBA
var swsCtx *astiav.SoftwareScaleContext
var dstFrame *astiav.Frame
// Get stream time base for timing calculations
timeBase := videoStream.TimeBase()
// Calculate frame duration for fixed fps mode
var frameDuration time.Duration
if fps > 0 {
frameDuration = time.Second / time.Duration(fps)
}
var rows int
var playbackStart time.Time // Wall clock time when playback started
var firstPTS int64 // PTS of the first frame
var frameCount int64 // Frame counter for fixed fps mode
var initialized bool
// Read packets
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Read frame
if err := formatCtx.ReadFrame(pkt); err != nil {
if errors.Is(err, astiav.ErrEof) {
return nil
}
return fmt.Errorf("mp4: reading frame failed: %w", err)
}
// Handle subtitle packets
if subtitleStream != nil && pkt.StreamIndex() == subtitleStreamIdx {
// Parse mov_text subtitle format (2-byte length prefix + UTF-8 text)
data := pkt.Data()
if len(data) >= 2 {
textLen := int(binary.BigEndian.Uint16(data[:2]))
if textLen > 0 && len(data) >= 2+textLen {
text := strings.TrimSpace(string(data[2 : 2+textLen]))
if text != "" {
// Convert subtitle PTS to video stream time base
pktPTS := pkt.Pts()
startPTS := pktPTS * int64(subtitleTimeBase.Num()) * int64(timeBase.Den()) / (int64(subtitleTimeBase.Den()) * int64(timeBase.Num()))
// Duration from packet, convert to video time base
pktDuration := pkt.Duration()
durationPTS := pktDuration * int64(subtitleTimeBase.Num()) * int64(timeBase.Den()) / (int64(subtitleTimeBase.Den()) * int64(timeBase.Num()))
endPTS := startPTS + durationPTS
subtitles = append(subtitles, subtitle{
startPTS: startPTS,
endPTS: endPTS,
text: text,
})
}
}
}
pkt.Unref()
continue
}
// Skip non-video packets
if pkt.StreamIndex() != videoStreamIdx {
pkt.Unref()
continue
}
// Send packet to decoder
if err := codecCtx.SendPacket(pkt); err != nil {
pkt.Unref()
continue
}
pkt.Unref()
// Receive frames from decoder
for {
if err := codecCtx.ReceiveFrame(frame); err != nil {
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
break
}
return fmt.Errorf("mp4: receiving frame failed: %w", err)
}
// Initialize scaler on first frame
if swsCtx == nil {
var err error
swsCtx, err = astiav.CreateSoftwareScaleContext(
frame.Width(),
frame.Height(),
frame.PixelFormat(),
frame.Width(),
frame.Height(),
astiav.PixelFormatRgba,
astiav.NewSoftwareScaleContextFlags(astiav.SoftwareScaleContextFlagBilinear),
)
if err != nil {
frame.Unref()
return fmt.Errorf("mp4: creating software scale context failed: %w", err)
}
defer swsCtx.Free()
dstFrame = astiav.AllocFrame()
if dstFrame == nil {
frame.Unref()
return fmt.Errorf("mp4: failed to allocate destination frame")
}
defer dstFrame.Free()
}
// Scale frame to RGBA
if err := swsCtx.ScaleFrame(frame, dstFrame); err != nil {
frame.Unref()
return fmt.Errorf("mp4: scaling frame failed: %w", err)
}
// Convert to Go image
img, err := dstFrame.Data().GuessImageFormat()
if err != nil {
frame.Unref()
return fmt.Errorf("mp4: guessing image format failed: %w", err)
}
if err := dstFrame.Data().ToImage(img); err != nil {
frame.Unref()
return fmt.Errorf("mp4: converting frame to image failed: %w", err)
}
// Initialize timing on first frame
if !initialized {
playbackStart = time.Now()
firstPTS = frame.Pts()
initialized = true
}
// Calculate target display time and wait if needed
var targetTime time.Time
if fps > 0 {
// Fixed framerate: target time based on frame count
targetTime = playbackStart.Add(time.Duration(frameCount) * frameDuration)
} else if fps < 0 {
// Native timing: target time based on PTS
ptsDiff := frame.Pts() - firstPTS
videoTime := time.Duration(float64(ptsDiff) * float64(timeBase.Num()) / float64(timeBase.Den()) * float64(time.Second))
targetTime = playbackStart.Add(videoTime)
}
// Sleep until target time (if we're ahead of schedule)
if sleepDuration := time.Until(targetTime); sleepDuration > 0 {
select {
case <-ctx.Done():
frame.Unref()
return ctx.Err()
case <-time.After(sleepDuration):
}
}
frameCount++
// Apply filters and convert to paletted
filteredImg := redraw(img, p.c.Filter, p.c.Drawer)
// Flush to output
if err := flush(p.w, filteredImg, p.c.Flusher); err != nil {
frame.Unref()
return err
}
// Calculate rows for cursor reset
if rows == 0 {
rows = filteredImg.Bounds().Dy() / 4
if filteredImg.Bounds().Dy()%4 != 0 {
rows++
}
}
// Overlay subtitle on the bottom rows of the braille output
imageWidthInChars := filteredImg.Bounds().Dx() / 2
currentPTS := frame.Pts()
activeSubtitle := findActiveSubtitle(subtitles, currentPTS)
if activeSubtitle != "" {
// Wrap and limit subtitle lines (max 3 lines, leave 1 line of braille visible at top minimum)
maxLines := 3
if rows-1 < maxLines {
maxLines = rows - 1
}
if maxLines < 1 {
maxLines = 1
}
lines := wrapText(activeSubtitle, imageWidthInChars, maxLines)
numLines := len(lines)
// Move cursor up to position subtitles at the bottom of the frame
// We go up (numLines) lines from the current position (which is after the last row)
fmt.Fprintf(p.w, "\033[%dA", numLines)
// Write each line centered (preserving braille on left and right)
for i, line := range lines {
// Calculate column position for centered text (1-indexed for ANSI)
lineLen := len([]rune(line))
startCol := (imageWidthInChars - lineLen) / 2
if startCol < 1 {
startCol = 1
}
// Move cursor to the start column and write text (preserves braille on sides)
fmt.Fprintf(p.w, "\033[%dG%s", startCol, line)
if i < numLines-1 {
fmt.Fprint(p.w, "\n")
}
}
// Move cursor back down to the original position
// We're now on the last subtitle line (which is the last row of the frame)
// So we only need to move down 1 line to get back to where we started
fmt.Fprint(p.w, "\033[1B\r")
}
p.c.Reset(p.w, rows)
frame.Unref()
}
}
}
// findActiveSubtitle returns the subtitle text that should be displayed at the given PTS.
func findActiveSubtitle(subtitles []subtitle, pts int64) string {
for _, s := range subtitles {
if pts >= s.startPTS && pts < s.endPTS {
return s.text
}
}
return ""
}
// wrapText wraps text to fit within width, limiting to maxLines.
// If text exceeds maxLines, the last line is truncated with ellipsis.
func wrapText(text string, width, maxLines int) []string {
if width <= 0 || maxLines <= 0 {
return nil
}
text = strings.TrimSpace(text)
if text == "" {
return nil
}
words := strings.Fields(text)
if len(words) == 0 {
return nil
}
var lines []string
var currentLine string
for _, word := range words {
wordRunes := []rune(word)
// If single word is longer than width, we need to handle it specially
if len(wordRunes) > width {
// Flush current line if not empty
if currentLine != "" {
lines = append(lines, currentLine)
currentLine = ""
}
// Break the long word across lines
for len(wordRunes) > 0 {
if len(lines) >= maxLines {
break
}
take := width
if take > len(wordRunes) {
take = len(wordRunes)
}
lines = append(lines, string(wordRunes[:take]))
wordRunes = wordRunes[take:]
}
continue
}
// Check if word fits on current line
if currentLine == "" {
currentLine = word
} else if len([]rune(currentLine))+1+len(wordRunes) <= width {
currentLine += " " + word
} else {
// Word doesn't fit, start new line
lines = append(lines, currentLine)
currentLine = word
}
}
// Don't forget the last line
if currentLine != "" {
lines = append(lines, currentLine)
}
// Limit to maxLines and add ellipsis if truncated
if len(lines) > maxLines {
lines = lines[:maxLines]
// Add ellipsis to last line
lastLine := []rune(lines[maxLines-1])
if len(lastLine)+3 <= width {
lines[maxLines-1] = string(lastLine) + "..."
} else if len(lastLine) > 3 {
lines[maxLines-1] = string(lastLine[:len(lastLine)-3]) + "..."
}
}
return lines
}
================================================
FILE: webcam.go
================================================
package dotmatrix
import (
"context"
"errors"
"fmt"
"io"
"strings"
"time"
"github.com/asticode/go-astiav"
)
// WebcamPrinter prints webcam frames as braille characters.
type WebcamPrinter struct {
w io.Writer
c Config
}
// NewWebcamPrinter creates a new WebcamPrinter.
func NewWebcamPrinter(w io.Writer, c *Config) *WebcamPrinter {
return &WebcamPrinter{
w: w,
c: mergeConfig(c),
}
}
// Print captures frames from the webcam and renders them as braille.
// If fps is less than zero, frames are rendered as fast as they arrive.
// Otherwise, fps dictates how many frames per second are printed.
func (p *WebcamPrinter) Print(ctx context.Context, fps int) error {
// Register all devices to enable AVFoundation
astiav.RegisterAllDevices()
// Find AVFoundation input format (macOS)
inputFormat := astiav.FindInputFormat("avfoundation")
if inputFormat == nil {
return fmt.Errorf("webcam: avfoundation input format not found (macOS only)")
}
// Set up options
options := astiav.NewDictionary()
defer options.Free()
// Set framerate - default to 30 if not specified (AVFoundation requires exact fps)
if fps <= 0 {
fps = 30
}
options.Set("framerate", fmt.Sprintf("%d", fps), 0)
// Set pixel format to one supported by most cameras
options.Set("pixel_format", "uyvy422", 0)
// Increase probesize to give AVFoundation time to initialize
options.Set("probesize", "32000000", 0)
// Allocate format context
formatCtx := astiav.AllocFormatContext()
if formatCtx == nil {
return fmt.Errorf("webcam: failed to allocate format context")
}
defer formatCtx.Free()
// Open webcam device (device "0" is the first video device)
if err := formatCtx.OpenInput("0", inputFormat, options); err != nil {
// Provide helpful error messages for common issues
errStr := err.Error()
if errStr == "Operation not permitted" || errStr == "Permission denied" {
return fmt.Errorf("webcam: permission denied - check System Preferences > Privacy & Security > Camera")
}
if errStr == "Device or resource busy" {
return fmt.Errorf("webcam: camera is in use by another application")
}
return fmt.Errorf("webcam: failed to open camera: %w", err)
}
defer formatCtx.CloseInput()
// Find stream info
if err := formatCtx.FindStreamInfo(nil); err != nil {
return fmt.Errorf("webcam: finding stream info failed: %w", err)
}
// Find video stream
var videoStream *astiav.Stream
var videoStreamIdx int
for _, s := range formatCtx.Streams() {
if s.CodecParameters().MediaType() == astiav.MediaTypeVideo {
videoStream = s
videoStreamIdx = s.Index()
break
}
}
if videoStream == nil {
return fmt.Errorf("webcam: no video stream found")
}
// Find decoder
codec := astiav.FindDecoder(videoStream.CodecParameters().CodecID())
if codec == nil {
return fmt.Errorf("webcam: decoder not found for codec %s", videoStream.CodecParameters().CodecID())
}
// Allocate codec context
codecCtx := astiav.AllocCodecContext(codec)
if codecCtx == nil {
return fmt.Errorf("webcam: failed to allocate codec context")
}
defer codecCtx.Free()
// Copy codec parameters
if err := videoStream.CodecParameters().ToCodecContext(codecCtx); err != nil {
return fmt.Errorf("webcam: copying codec parameters failed: %w", err)
}
// Open codec
if err := codecCtx.Open(codec, nil); err != nil {
return fmt.Errorf("webcam: opening codec failed: %w", err)
}
// Allocate packet
pkt := astiav.AllocPacket()
if pkt == nil {
return fmt.Errorf("webcam: failed to allocate packet")
}
defer pkt.Free()
// Allocate frame
frame := astiav.AllocFrame()
if frame == nil {
return fmt.Errorf("webcam: failed to allocate frame")
}
defer frame.Free()
// Create software scale context for converting to RGBA
var swsCtx *astiav.SoftwareScaleContext
var dstFrame *astiav.Frame
// Calculate frame duration for fps limiting
var frameDuration time.Duration
if fps > 0 {
frameDuration = time.Second / time.Duration(fps)
}
var rows int
var lastFrameTime time.Time
// Read frames
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// Read frame
if err := formatCtx.ReadFrame(pkt); err != nil {
if errors.Is(err, astiav.ErrEof) {
return nil
}
// EAGAIN means no frame available yet, just retry
if errors.Is(err, astiav.ErrEagain) {
continue
}
// Also check error string for "Resource temporarily unavailable" (EAGAIN on some systems)
if strings.Contains(err.Error(), "Resource temporarily unavailable") {
continue
}
return fmt.Errorf("webcam: reading frame failed: %w", err)
}
// Skip non-video packets
if pkt.StreamIndex() != videoStreamIdx {
pkt.Unref()
continue
}
// Send packet to decoder
if err := codecCtx.SendPacket(pkt); err != nil {
pkt.Unref()
continue
}
pkt.Unref()
// Receive frames from decoder
for {
if err := codecCtx.ReceiveFrame(frame); err != nil {
if errors.Is(err, astiav.ErrEof) || errors.Is(err, astiav.ErrEagain) {
break
}
return fmt.Errorf("webcam: receiving frame failed: %w", err)
}
// Initialize scaler on first frame
if swsCtx == nil {
var err error
swsCtx, err = astiav.CreateSoftwareScaleContext(
frame.Width(),
frame.Height(),
frame.PixelFormat(),
frame.Width(),
frame.Height(),
astiav.PixelFormatRgba,
astiav.NewSoftwareScaleContextFlags(astiav.SoftwareScaleContextFlagBilinear),
)
if err != nil {
frame.Unref()
return fmt.Errorf("webcam: creating software scale context failed: %w", err)
}
defer swsCtx.Free()
dstFrame = astiav.AllocFrame()
if dstFrame == nil {
frame.Unref()
return fmt.Errorf("webcam: failed to allocate destination frame")
}
defer dstFrame.Free()
}
// Rate limiting for fps > 0
if fps > 0 && !lastFrameTime.IsZero() {
elapsed := time.Since(lastFrameTime)
if sleepDuration := frameDuration - elapsed; sleepDuration > 0 {
select {
case <-ctx.Done():
frame.Unref()
return ctx.Err()
case <-time.After(sleepDuration):
}
}
}
lastFrameTime = time.Now()
// Scale frame to RGBA
if err := swsCtx.ScaleFrame(frame, dstFrame); err != nil {
frame.Unref()
return fmt.Errorf("webcam: scaling frame failed: %w", err)
}
// Convert to Go image
img, err := dstFrame.Data().GuessImageFormat()
if err != nil {
frame.Unref()
return fmt.Errorf("webcam: guessing image format failed: %w", err)
}
if err := dstFrame.Data().ToImage(img); err != nil {
frame.Unref()
return fmt.Errorf("webcam: converting frame to image failed: %w", err)
}
// Apply filters and convert to paletted
filteredImg := redraw(img, p.c.Filter, p.c.Drawer)
// Flush to output
if err := flush(p.w, filteredImg, p.c.Flusher); err != nil {
frame.Unref()
return err
}
// Calculate rows for cursor reset
if rows == 0 {
rows = filteredImg.Bounds().Dy() / 4
if filteredImg.Bounds().Dy()%4 != 0 {
rows++
}
}
p.c.Reset(p.w, rows)
frame.Unref()
}
}
}
gitextract_fwtecarx/ ├── .github/ │ └── workflows/ │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── braille.go ├── doc.go ├── dotmatrix_suite_test.go ├── ffmpeg_check.go ├── gif.go ├── go.mod ├── go.sum ├── image.go ├── image_test.go ├── mise.toml ├── mp4.go └── webcam.go
SYMBOL INDEX (32 symbols across 6 files)
FILE: braille.go
type Braille (line 16) | type Braille
method Rune (line 27) | func (b Braille) Rune() rune {
method String (line 38) | func (b Braille) String() string {
type BrailleFlusher (line 42) | type BrailleFlusher struct
method Flush (line 44) | func (BrailleFlusher) Flush(w io.Writer, img image.Image) error {
FILE: dotmatrix_suite_test.go
function TestDotmatrix (line 10) | func TestDotmatrix(t *testing.T) {
FILE: gif.go
type GIFPrinter (line 12) | type GIFPrinter struct
method Print (line 27) | func (p *GIFPrinter) Print(ctx context.Context, giff *gif.GIF) error {
method drawOver (line 97) | func (p *GIFPrinter) drawOver(target *image.Paletted, source image.Ima...
method drawExact (line 111) | func (p *GIFPrinter) drawExact(target *image.Paletted, source image.Im...
function NewGIFPrinter (line 17) | func NewGIFPrinter(w io.Writer, c *Config) *GIFPrinter {
FILE: image.go
type Flusher (line 12) | type Flusher interface
type Filter (line 18) | type Filter interface
type noop (line 22) | type noop struct
method Filter (line 24) | func (noop) Filter(img image.Image) image.Image {
type Config (line 28) | type Config struct
function mergeConfig (line 43) | func mergeConfig(c *Config) Config {
type Printer (line 66) | type Printer struct
method Print (line 123) | func (p *Printer) Print(img image.Image) error {
function Print (line 71) | func Print(w io.Writer, img image.Image) error {
function NewPrinter (line 76) | func NewPrinter(w io.Writer, c *Config) *Printer {
function redraw (line 128) | func redraw(img image.Image, filter Filter, drawer draw.Drawer) *image.P...
function flush (line 149) | func flush(w io.Writer, img image.Image, flusher Flusher) error {
FILE: mp4.go
type subtitle (line 16) | type subtitle struct
type MP4Printer (line 23) | type MP4Printer struct
method Print (line 39) | func (p *MP4Printer) Print(ctx context.Context, inputPath string, fps ...
function NewMP4Printer (line 29) | func NewMP4Printer(w io.Writer, c *Config) *MP4Printer {
function findActiveSubtitle (line 349) | func findActiveSubtitle(subtitles []subtitle, pts int64) string {
function wrapText (line 360) | func wrapText(text string, width, maxLines int) []string {
FILE: webcam.go
type WebcamPrinter (line 15) | type WebcamPrinter struct
method Print (line 31) | func (p *WebcamPrinter) Print(ctx context.Context, fps int) error {
function NewWebcamPrinter (line 21) | func NewWebcamPrinter(w io.Writer, c *Config) *WebcamPrinter {
Condensed preview — 17 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (82K chars).
[
{
"path": ".github/workflows/release.yml",
"chars": 5966,
"preview": "name: Release\n\non:\n push:\n tags:\n - 'v*'\n\npermissions:\n contents: write\n\njobs:\n release:\n strategy:\n "
},
{
"path": ".github/workflows/test.yml",
"chars": 686,
"preview": "name: Test\n\non:\n pull_request:\n branches: [master, main]\n\njobs:\n test:\n runs-on: ubuntu-24.04\n steps:\n -"
},
{
"path": ".gitignore",
"chars": 29,
"preview": "_ignore/\n.DS_Store\ndotmatrix\n"
},
{
"path": "LICENSE.md",
"chars": 1081,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Kevin Cantwell\n\nPermission is hereby granted, free of charge, to any person ob"
},
{
"path": "README.md",
"chars": 5849,
"preview": "# Dotmatrix\n\n[](https://pkg.go.dev/gith"
},
{
"path": "braille.go",
"chars": 2091,
"preview": "package dotmatrix\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"io\"\n)\n\n// Braille epresents an 8 dot braille pattern in x,y coordi"
},
{
"path": "doc.go",
"chars": 5812,
"preview": "/*\nPackage dotmatrix encodes images in a \"dot matrix\" pattern using braille unicode characters.\nImages are first convert"
},
{
"path": "dotmatrix_suite_test.go",
"chars": 200,
"preview": "package dotmatrix_test\n\nimport (\n\t. \"github.com/onsi/ginkgo\"\n\t. \"github.com/onsi/gomega\"\n\n\t\"testing\"\n)\n\nfunc TestDotmatr"
},
{
"path": "ffmpeg_check.go",
"chars": 851,
"preview": "package dotmatrix\n\n/*\n#cgo pkg-config: libavcodec libavformat libavutil\n\n#include <libavcodec/avcodec.h>\n#include <libav"
},
{
"path": "gif.go",
"chars": 2891,
"preview": "package dotmatrix\n\nimport (\n\t\"context\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/gif\"\n\t\"io\"\n\t\"time\"\n)\n\ntype GIFPrinter struct {\n\tw"
},
{
"path": "go.mod",
"chars": 921,
"preview": "module github.com/kevin-cantwell/dotmatrix\n\ngo 1.22.0\n\ntoolchain go1.22.2\n\nrequire (\n\tgithub.com/asticode/go-astiav v0.4"
},
{
"path": "go.sum",
"chars": 11468,
"preview": "github.com/asticode/go-astiav v0.40.0 h1:7El8FyONtjPOmcmqR+zjjihG7ysum5RtTVIhIMrLFQM=\ngithub.com/asticode/go-astiav v0.4"
},
{
"path": "image.go",
"chars": 5189,
"preview": "package dotmatrix\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"io\"\n)\n\n// Flushes an image to the io.Writer. "
},
{
"path": "image_test.go",
"chars": 1025,
"preview": "package dotmatrix\n\n// import (\n// \t. \"github.com/onsi/ginkgo\"\n// \t. \"github.com/onsi/gomega\"\n// )\n\n// // ⡪⣛\n// //\n// var"
},
{
"path": "mise.toml",
"chars": 20,
"preview": "[tools]\ngo = \"1.22\"\n"
},
{
"path": "mp4.go",
"chars": 11659,
"preview": "package dotmatrix\n\nimport (\n\t\"context\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/astico"
},
{
"path": "webcam.go",
"chars": 7093,
"preview": "package dotmatrix\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/asticode/go-astiav\"\n)\n\n//"
}
]
About this extraction
This page contains the full source code of the kevin-cantwell/dotmatrix GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 17 files (61.4 KB), approximately 35.0k tokens, and a symbol index with 32 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.