Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: wbrown/img2ansi
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.1-beta
Choose a base ref
...
head repository: wbrown/img2ansi
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 3 commits
  • 29 files changed
  • 1 contributor

Commits on Aug 5, 2024

  1. Create LICENSE

    wbrown authored Aug 5, 2024

    Verified

    This commit was signed with the committer’s verified signature.
    Copy the full SHA
    020dcc5 View commit details

Commits on Aug 7, 2024

  1. Optimized, more precise KD Search, embedded binary precomputations an…

    …d palettes, more color space, palette marshalling and unmarshalling.
    wbrown committed Aug 7, 2024
    Copy the full SHA
    7ac0692 View commit details
  2. Copy the full SHA
    259f9a5 View commit details
28 changes: 28 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
BSD 3-Clause License

Copyright (c) 2024, Wes Brown

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
40 changes: 26 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
@@ -32,6 +32,9 @@ and text-based display.
6. **Optimized for Text Output**: Designed to produce ANSI escape code
sequences, making it ideal for terminal-based image display.

7. **Optimized KD Tree Search**: Optimized for ANSI art generation by
precomputing quantized color distances.

## How It Works

The algorithm processes the input image in 2x2 blocks, determining the best
@@ -60,13 +63,14 @@ uses the 256 color scheme.
To build the program, run the following commands:

```sh
go get -u github.com/wbrown/ansi2img
go build github.com/wbrown/ansi2img/cmd/ansify
```

## Usage
`./img2ansi -input <input> [-output <output>] [-width <width>]
[-scale <scale>] [-quantization <quantization>] [-maxchars <maxchars>]
[-8bit] [-jb] [-table]`
[-color_method <color_method>] [-palette <palette>] [-kdsearch <kdsearch>]
[-cache_threshold <cache_threshold>]`

**Performance**

@@ -90,19 +94,29 @@ image. The cache is used to speed up the program by not having to recompute
the blocks for each 2x2 pixel block in the image. It is a fuzzy cache, so it
is thresholded on error distance from the target block.

There are built in embedded palettes that have precomputed tables for the
colors. These are `ansi16`, `ansi256`, and `jetbrains32`. Each precomputed
palette also has three color spaces that are precomputed: `RGB`, `Lab`, and
`Redmean`. The default is `Redmean`.

**Colors**

By default the program uses the 16-color ANSI palette, split into 8 foreground
colors and 8 background colors. The `-8bit` option can be used to enable 256
color mode. The `-jb` option can be used to use the JetBrains color scheme,
which allows for separate foreground and background palettes to effectively
double the number of colors available.

The program performes well without quantization, but if you want to reduce the
colors and 8 background colors. There are three palettes built in, selectable
by using the `-palette` option:
* `ansi16`: The default 16-color ANSI palette
* `ansi256`: The 256-color ANSI palette
* `jetbrains32`: The JetBrains color scheme that uses 32 colors by having
separate palettes for foreground and background colors.
The program performs well without quantization, but if you want to reduce the
number of colors in the output, you can use the `-quantization` option. The
default is `256` colors. This isn't the output colors, but the number of
colors used in the quantization step.

There are three color space options available: `RGB`, `Lab`, and `Redmean`.
The most perceptually accurate is `Lab`, but it is also the slowest. The
default is `Redmean`.

**Image Size**

The `-width` option can be used to set the target width of the output image,
@@ -111,26 +125,24 @@ default `-scale` is `2`, which approximately halves the height of the output,
to compensate for the fact that characters are taller than they are wide.

```
-8bit
Use 8-bit ANSI colors (256 colors)
-cache_threshold float
Threshold for block cache (default 40)
-colormethod string
Color distance method: RGB, LAB, or Redmean (default "RGB")
-input string
Path to the input image file (required)
-jb
Use JetBrains color scheme
-kdsearch int
Number of nearest neighbors to search in KD-tree, 0 to disable (default 50)
-maxchars int
Maximum number of characters in the output (default 1048576)
-output string
Path to save the output (if not specified, prints to stdout)
-palette string
Path to the palette file (Embedded: ansi16, ansi256, jetbrains32) (default "ansi16")
-quantization int
Quantization factor (default 256)
-scale float
Scale factor for the output image (default 2)
-table
Print ANSI color table
-width int
Target width of the output image (default 80)
```
27 changes: 24 additions & 3 deletions ansi.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
package main
package img2ansi

import (
"fmt"
"strings"
)

// compressANSI compresses an ANSI image by combining adjacent blocks with
type AnsiEntry struct {
Key uint32
Value string
}
type AnsiData []AnsiEntry

var (
fgAnsi = NewOrderedMap()
bgAnsi = NewOrderedMap()
)

// CompressANSI compresses an ANSI image by combining adjacent blocks with
// the same foreground and background colors. The function takes an ANSI
// image as a string and returns the more efficient ANSI image as a string.
func compressANSI(ansiImage string) string {
func CompressANSI(ansiImage string) string {
var compressed strings.Builder
var currentFg, currentBg, currentBlock string
var count int
@@ -124,6 +135,16 @@ func colorIsBackground(color string) bool {
color == "48"
}

// ToOrderedMap converts an AnsiData slice to an OrderedMap with the values
// as keys and the keys as values.
func (ansiData AnsiData) ToOrderedMap() *OrderedMap {
om := NewOrderedMap()
for _, entry := range ansiData {
om.Set(entry.Key, entry.Value)
}
return om
}

// renderToAnsi renders a 2D array of BlockRune structs to an ANSI string.
// It does not perform any compression or optimization.
func renderToAnsi(blocks [][]BlockRune) string {
30 changes: 15 additions & 15 deletions approximatecache.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package img2ansi

import (
"math"
@@ -8,22 +8,22 @@ import (
// that is used to store approximate matches for a given
// block of 4 RGB values. Approximate matches are performed
// by comparing the error of a given match to a threshold
// value.
// Value.
//
// The key of the map is a Uint256, which is a 256-bit
// The Key of the map is a Uint256, which is a 256-bit
// unsigned integer that is used to represent the foreground
// and background colors of a block of 4 RGB values.
//
// There may be multiple matches for a given key, so the
// value of the map is a lookupEntry, which is a struct
// There may be multiple matches for a given Key, so the
// Value of the map is a lookupEntry, which is a struct
// that contains a slice of Match structs.
type ApproximateCache map[Uint256]lookupEntry

// Match is a struct that contains the rune, foreground
// color, background color, and error of a match. The error
// is a float64 value that represents the difference between
// is a float64 Value that represents the difference between
// the actual block of 4 RGB values and the pair of foreground
// and background colors encoded in the key as an Uint256.
// and background colors encoded in the Key as an Uint256.
type Match struct {
Rune rune
FG RGB
@@ -36,7 +36,7 @@ type lookupEntry struct {
}

// AddEntry adds a new entry to the cache. The entry is
// represented by a key, which is a Uint256, and a Match
// represented by a Key, which is a Uint256, and a Match
// struct that contains the rune, foreground color, background
// color, and error of the match.
func (cache ApproximateCache) addEntry(
@@ -70,19 +70,19 @@ func (cache ApproximateCache) addEntry(
}

// GetEntry retrieves an entry from the cache. The entry is
// represented by a key, which is a Uint256, and a block of
// represented by a Key, which is a Uint256, and a block of
// 4 RGB values. The function returns the rune, foreground
// color, background color, and a boolean value indicating
// color, background color, and a boolean Value indicating
// whether the entry was found in the cache.
//
// There may be multiple matches for a given key, so the
// function returns the match with the lowest error value.
// There may be multiple matches for a given Key, so the
// function returns the match with the lowest error Value.
func (cache ApproximateCache) getEntry(
k Uint256,
block [4]RGB,
isEdge bool,
) (rune, RGB, RGB, bool) {
baseThreshold := cacheThreshold
baseThreshold := CacheThreshold
if isEdge {
baseThreshold *= 0.7
}
@@ -101,10 +101,10 @@ func (cache ApproximateCache) getEntry(
}
}
if bestMatch != nil {
lookupHits++
LookupHits++
return bestMatch.Rune, bestMatch.FG, bestMatch.BG, true
}
}
lookupMisses++
LookupMisses++
return 0, RGB{}, RGB{}, false
}
2 changes: 1 addition & 1 deletion attic.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package img2ansi

// Below are measured from the terminal using a color picker
//ansiOverrides = map[uint32]string{
Loading